Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as patches | |
| import math | |
| import rectpack | |
| import os | |
| # ============================================================================== | |
| # 1. CORE OPTIMIZATION LOGIC (Unchanged) | |
| # ============================================================================== | |
| def find_waste_rectangles(sheet_w, sheet_h, packer): | |
| empty_spaces = [(0, 0, sheet_w, sheet_h)] | |
| if not packer or not packer[0]: return empty_spaces | |
| packed_rects_list = packer[0] | |
| for p_rect in packed_rects_list: | |
| prx, pry, prw, prh = p_rect.x, p_rect.y, p_rect.width, p_rect.height | |
| newly_created_spaces = [] | |
| for i in range(len(empty_spaces) - 1, -1, -1): | |
| space = empty_spaces.pop(i) | |
| sx, sy, sw, sh = space | |
| if not (prx + prw <= sx or prx >= sx + sw or pry + prh <= sy or pry >= sy + sh): | |
| if prx > sx: newly_created_spaces.append((sx, sy, prx - sx, sh)) | |
| if prx + prw < sx + sw: newly_created_spaces.append((prx + prw, sy, (sx + sw) - (prx + prw), sh)) | |
| if pry > sy: newly_created_spaces.append((sx, sy, sw, pry - sy)) | |
| if pry + prh < sy + sh: newly_created_spaces.append((sx, pry + prh, sw, (sy + sh) - (pry + prh))) | |
| else: | |
| empty_spaces.append(space) | |
| empty_spaces.extend(newly_created_spaces) | |
| return [s for s in empty_spaces if s[2] > 0.01 and s[3] > 0.01] | |
| def attempt_to_pack(sheet_w, sheet_h, box_w, box_h, num_pieces_to_try, allow_rotation): | |
| if num_pieces_to_try <= 0: return None | |
| rectangles = [(box_w, box_h)] * num_pieces_to_try | |
| packer_algorithms = [rectpack.MaxRectsBaf, rectpack.MaxRectsBlsf, rectpack.MaxRectsBssf, rectpack.SkylineMwf, rectpack.GuillotineBssfLas] | |
| for algo in packer_algorithms: | |
| try: | |
| packer = rectpack.newPacker(rotation=allow_rotation, pack_algo=algo) | |
| packer.add_bin(sheet_w, sheet_h) | |
| for r in rectangles: packer.add_rect(*r) | |
| packer.pack() | |
| if len(packer[0]) == num_pieces_to_try: | |
| return {'packer': packer, 'strategy': algo.__name__} | |
| except Exception: | |
| continue | |
| return None | |
| def find_max_layout(sheet_w, sheet_h, box_w, box_h, allow_rotation): | |
| theoretical_max = math.floor((sheet_w * sheet_h) / (box_w * box_h)) if (box_w * box_h) > 0 else 0 | |
| low, high = 0, min(theoretical_max, 1000) | |
| best_confirmed_result = {'pieces': 0, 'packer': None, 'strategy': 'None'} | |
| while low <= high: | |
| mid = (low + high) // 2 | |
| if mid == 0: | |
| low = mid + 1 | |
| continue | |
| result = attempt_to_pack(sheet_w, sheet_h, box_w, box_h, mid, allow_rotation) | |
| if result: | |
| best_confirmed_result = {'pieces': mid, **result} | |
| low = mid + 1 | |
| else: | |
| high = mid - 1 | |
| return best_confirmed_result | |
| def analyze_single_sheet(sheet_w, sheet_h, box_w, box_h, gsm, price_per_kg): | |
| result_no_rotation = find_max_layout(sheet_w, sheet_h, box_w, box_h, allow_rotation=False) | |
| result_no_rotation['rotation_used'] = False | |
| result_with_rotation = {'pieces': 0} | |
| if box_w != box_h: | |
| result_with_rotation = find_max_layout(sheet_w, sheet_h, box_w, box_h, allow_rotation=True) | |
| result_with_rotation['rotation_used'] = True | |
| final_winner = result_no_rotation if result_no_rotation['pieces'] >= result_with_rotation['pieces'] else result_with_rotation | |
| if not final_winner.get('packer'): | |
| final_winner['pieces'] = 0 | |
| else: | |
| final_winner['pieces'] = len(final_winner['packer'][0]) | |
| if final_winner['pieces'] == 0: return None | |
| SQ_IN_TO_SQ_M = 0.00064516 | |
| sheet_area_sq_m = (sheet_w * sheet_h) * SQ_IN_TO_SQ_M | |
| sheet_weight_kg = (sheet_area_sq_m * gsm) / 1000 | |
| sheet_cost_rs = sheet_weight_kg * price_per_kg | |
| pieces_per_sheet = final_winner['pieces'] | |
| piece_area, sheet_area = box_w * box_h, sheet_w * sheet_h | |
| total_piece_area = pieces_per_sheet * piece_area | |
| waste_area = sheet_area - total_piece_area | |
| utilization_percent = (total_piece_area / sheet_area) * 100 if sheet_area > 0 else 0 | |
| theoretical_max = math.floor(sheet_area / piece_area) if piece_area > 0 else 0 | |
| efficiency_rating = (pieces_per_sheet / theoretical_max) * 100 if theoretical_max > 0 else 0 | |
| final_winner.update({ | |
| 'sheet_w': sheet_w, 'sheet_h': sheet_h, 'box_w': box_w, 'box_h': box_h, 'sheet_cost_rs': sheet_cost_rs, | |
| 'sheet_weight_kg': sheet_weight_kg, 'waste_per_sheet_sq_in': waste_area, 'utilization_percent': utilization_percent, | |
| 'efficiency_rating': efficiency_rating, 'theoretical_max_pieces': theoretical_max, | |
| 'true_cost_per_piece_rs': sheet_cost_rs / pieces_per_sheet if pieces_per_sheet > 0 else 0, | |
| 'waste_cost_per_sheet': (waste_area / sheet_area) * sheet_cost_rs if sheet_area > 0 else 0, | |
| }) | |
| return final_winner | |
| # ============================================================================== | |
| # 2. GRADIO-SPECIFIC FUNCTIONS (Visualization and Control) | |
| # ============================================================================== | |
| def visualize_pattern_for_gradio(result, rank=1): | |
| """ | |
| FIXED: The plot title now clearly displays the sheet size and rank icon. | |
| """ | |
| if not result or not result.get('packer'): | |
| fig, ax = plt.subplots(1, 1, figsize=(10, 8)); ax.text(0.5, 0.5, 'No layout data available', ha='center', va='center', fontsize=16, color='gray') | |
| ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off'); plt.close(fig) | |
| return fig | |
| sheet_w, sheet_h = result['sheet_w'], result['sheet_h'] | |
| packer, pieces_per_sheet = result['packer'], result['pieces'] | |
| fig_width = min(12, max(8, sheet_w / 3)); fig_height = min(10, max(6, sheet_h / 3)) + 3.5 | |
| fig, ax = plt.subplots(1, 1, figsize=(fig_width, fig_height)) | |
| rank_icon = "π₯" if rank == 1 else "π₯" if rank == 2 else "π₯" | |
| title_color = '#2E8B57' if rank == 1 else '#4682B4' if rank == 2 else '#CD853F' | |
| fig.suptitle(f"{rank_icon} Layout for {sheet_w}β³ Γ {sheet_h}β³ Sheet", fontsize=18, fontweight='bold', color=title_color, y=0.98) | |
| waste_rects = find_waste_rectangles(sheet_w, sheet_h, packer) | |
| for (wx, wy, ww, wh) in waste_rects: | |
| ax.add_patch(patches.Rectangle((wx, wy), ww, wh, facecolor='#FFE4E1', edgecolor='#CD5C5C', linestyle='--', linewidth=1.5, alpha=0.8)) | |
| if ww > 1.5 and wh > 1.5: ax.text(wx + ww/2, wy + wh/2, f"{ww:.1f}Γ{wh:.1f}", ha='center', va='center', fontsize=8, color='#8B0000', fontweight='bold') | |
| colors = ['#4169E1', '#32CD32', '#FF6347', '#FFD700', '#9370DB', '#20B2AA'] | |
| for i, rect in enumerate(packer[0]): | |
| x, y, w, h = rect.x, rect.y, rect.width, rect.height | |
| ax.add_patch(patches.Rectangle((x, y), w, h, linewidth=2, edgecolor='black', facecolor=colors[i % len(colors)], alpha=0.8)) | |
| ax.text(x + w/2, y + h/2, f'#{i+1}\n{w:.1f}Γ{h:.1f}', ha='center', va='center', fontsize=9, color='white', fontweight='bold', bbox=dict(boxstyle="round,pad=0.1", facecolor='black', alpha=0.3)) | |
| legend_text = (f"**PERFORMANCE**\n- Pieces Fitted: {pieces_per_sheet} / {result.get('theoretical_max_pieces', 'N/A')} (theoretical)\n- Utilization: {result.get('utilization_percent', 0):.1f}% ({result.get('efficiency_rating', 0):.1f}% efficiency)\n\n**COST ANALYSIS**\n- Cost per Piece: Rs. {result['true_cost_per_piece_rs']:.3f}\n- Sheet Cost: Rs. {result['sheet_cost_rs']:.2f}\n\n**CONFIGURATION**\n- Algorithm: {result.get('strategy', 'Unknown')}\n- Rotation: {'Enabled' if result.get('rotation_used', False) else 'Disabled'}") | |
| props = dict(boxstyle='round,pad=0.5', facecolor='#F0F8FF', edgecolor='#4682B4', alpha=0.95, linewidth=2) | |
| fig.text(0.5, 0.01, legend_text, transform=plt.gcf().transFigure, fontsize=10, verticalalignment='bottom', horizontalalignment='center', bbox=props, fontfamily='monospace') | |
| ax.set_xlim(-0.5, sheet_w + 0.5); ax.set_ylim(-0.5, sheet_h + 0.5); ax.set_aspect('equal', adjustable='box'); ax.set_xlabel('Width (inches)', fontsize=12, fontweight='bold'); ax.set_ylabel('Height (inches)', fontsize=12, fontweight='bold'); ax.grid(True, alpha=0.3, linestyle=':'); ax.set_facecolor('#FAFAFA'); ax.add_patch(patches.Rectangle((0, 0), sheet_w, sheet_h, linewidth=3, edgecolor='black', facecolor='none')) | |
| plt.tight_layout(rect=[0, 0.15, 1, 0.95]); plt.close(fig) | |
| return fig | |
| def run_optimizer(gsm, price_per_kg, box_w, box_h, total_pieces_needed, mode, custom_w, custom_h, progress=gr.Progress()): | |
| market_sheets = [(18, 24), (20, 30), (22, 28), (25, 30), (23, 36), (25, 36), (30, 30), (30, 36)] | |
| sheets_to_check = [(custom_w, custom_h)] if mode == "Manual (Custom Size)" else market_sheets | |
| if any(val is None or val <= 0 for val in [gsm, price_per_kg, box_w, box_h, total_pieces_needed]): | |
| return create_error_summary("Please fill in all required fields with values greater than zero."), None, None, None | |
| if mode == "Manual (Custom Size)" and any(val is None or val <= 0 for val in [custom_w, custom_h]): | |
| return create_error_summary("For Manual mode, please provide custom sheet dimensions greater than zero."), None, None, None | |
| all_results = [] | |
| progress(0.1, desc="π Initializing analysis...") | |
| for i, (sw, sh) in enumerate(sheets_to_check): | |
| progress(0.1 + (i / len(sheets_to_check)) * 0.7, desc=f"π Analyzing {sw}Γ{sh} sheet...") | |
| if max(box_w, box_h) > max(sw, sh) and min(box_w, box_h) > min(sw,sh): | |
| if not (max(box_w, box_h) <= max(sw, sh) or min(box_w, box_h) <= min(sw,sh)): continue | |
| result = analyze_single_sheet(sw, sh, box_w, box_h, gsm, price_per_kg) | |
| if result: all_results.append(result) | |
| progress(0.85, desc="π Processing results...") | |
| if not all_results: | |
| return create_error_summary("No viable options found. The piece may be too large for the sheets."), None, None, None | |
| all_results.sort(key=lambda x: (x['true_cost_per_piece_rs'], -x['utilization_percent'])) | |
| progress(0.95, desc="π¨ Generating visualizations...") | |
| summary = create_data_rich_summary(all_results, total_pieces_needed) | |
| plots = [visualize_pattern_for_gradio(res, rank=i+1) for i, res in enumerate(all_results[:3])] | |
| while len(plots) < 3: plots.append(None) | |
| progress(1.0, desc="β Analysis complete!") | |
| return summary, plots[0], plots[1], plots[2] | |
| def create_error_summary(message): | |
| return f"""# β οΈ Analysis Error\n**{message}**\n\n## π‘ Troubleshooting Tips:\n- Ensure all input values are positive numbers.\n- Check that box dimensions can physically fit on the selected sheet sizes.""" | |
| def create_data_rich_summary(results, total_pieces_needed): | |
| """ | |
| FIXED: This function now generates a highly detailed, data-rich summary | |
| with all the metrics you requested, formatted beautifully for mobile. | |
| """ | |
| if not results: return create_error_summary("No viable layouts found.") | |
| best_result = results[0] | |
| sheets_needed = math.ceil(total_pieces_needed / best_result['pieces']) | |
| total_cost = sheets_needed * best_result['sheet_cost_rs'] | |
| total_weight_kg = sheets_needed * best_result['sheet_weight_kg'] | |
| total_waste_sq_in = sheets_needed * best_result['waste_per_sheet_sq_in'] | |
| sheet_area = best_result['sheet_w'] * best_result['sheet_h'] | |
| total_waste_kg = (total_waste_sq_in / sheet_area) * best_result['sheet_weight_kg'] if sheet_area > 0 else 0 | |
| summary = f""" | |
| <div class="report-title"> | |
| <h2><img src="https://img.icons8.com/office/40/000000/goal.png" style="vertical-align: middle; height: 32px;"> PRODUCTION OPTIMIZATION REPORT</h2> | |
| <h3><img src="https://img.icons8.com/fluency/48/000000/trophy.png" style="vertical-align: middle; height: 28px;"> RECOMMENDED SOLUTION</h3> | |
| </div> | |
| <div class="summary-card best-choice"> | |
| <h3>π₯ Best Choice: {best_result['sheet_w']}β³ Γ {best_result['sheet_h']}β³ Sheet</h3> | |
| <ul> | |
| <li><strong>π° Cost per Box:</strong> Rs. {best_result['true_cost_per_piece_rs']:.3f}</li> | |
| <li><strong>π¦ Pieces per Sheet:</strong> {best_result['pieces']}</li> | |
| <li><strong>π Sheet Utilization:</strong> {best_result['utilization_percent']:.1f}%</li> | |
| <li><strong>βοΈ Layout Efficiency:</strong> {best_result['efficiency_rating']:.1f}%</li> | |
| </ul> | |
| <h4>π Production Plan ({total_pieces_needed:,} pieces)</h4> | |
| <ul> | |
| <li><strong>π Total Sheets Needed:</strong> {sheets_needed:,}</li> | |
| <li><strong>βοΈ Total Paper to Buy:</strong> {total_weight_kg:.2f} kg</li> | |
| <li><strong>π΅ Total Investment:</strong> Rs. {total_cost:,.2f}</li> | |
| </ul> | |
| <h4>ποΈ Waste Analysis (Total)</h4> | |
| <ul> | |
| <li><strong>π Total Waste Area:</strong> {total_waste_sq_in:,.1f} sq. inches</li> | |
| <li><strong>βοΈ Total Waste Weight:</strong> {total_waste_kg:.2f} kg</li> | |
| </ul> | |
| </div> | |
| --- | |
| <h3 class="report-title"><img src="https://img.icons8.com/office/40/000000/bar-chart.png" style="vertical-align: middle; height: 28px;"> ALL VIABLE OPTIONS (RANKED)</h3> | |
| """ | |
| for i, res in enumerate(results): | |
| rank_icon = "π₯" if i == 0 else "π₯" if i == 1 else "π₯" if i == 2 else f"<b>#{i+1}</b>" | |
| summary += f""" | |
| <div class="summary-card"> | |
| <h4>{rank_icon} Sheet: {res['sheet_w']}β³ Γ {res['sheet_h']}β³</h4> | |
| <ul> | |
| <li><strong>π° Cost per Box:</strong> Rs. {res['true_cost_per_piece_rs']:.3f}</li> | |
| <li><strong>π¦ Pieces per Sheet:</strong> {res['pieces']}</li> | |
| <li><strong>π Utilization:</strong> {res['utilization_percent']:.1f}%</li> | |
| </ul> | |
| </div> | |
| """ | |
| return summary | |
| def update_ui_visibility(mode): | |
| return gr.Group(visible=(mode == "Manual (Custom Size)")) | |
| # ============================================================================== | |
| # 3. GLOBAL STATE & AUTHENTICATION (Unchanged) | |
| # ============================================================================== | |
| def check_password(password): | |
| correct_password = os.getenv("passwordApp", "defaultpassword123") | |
| if password == correct_password: | |
| return {login_status: gr.update(value="β **Access Granted!** Loading optimizer...", visible=True, elem_classes=["success-message"]), main_interface: gr.update(visible=True), login_interface: gr.update(visible=False)} | |
| return {login_status: gr.update(value="β **Incorrect Password!** Please try again.", visible=True, elem_classes=["error-message"]), password_input: gr.update(value="")} | |
| def logout_user(): | |
| return {main_interface: gr.update(visible=False), login_interface: gr.update(visible=True), password_input: gr.update(value=""), login_status: gr.update(value="", visible=False)} | |
| # ============================================================================== | |
| # 4. SUPER ENHANCED UI WITH MOBILE-FIRST RESPONSIVE DESIGN | |
| # ============================================================================== | |
| mobile_responsive_css = """ | |
| /* --- Global Styles --- */ | |
| .login-container { max-width: 450px; margin: 100px auto; padding: 40px; background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.2); } | |
| .login-title { background: linear-gradient(45deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; font-size: 28px; font-weight: bold; margin-bottom: 20px; } | |
| .main-container { background: #FFFFFF; border-radius: 15px; margin: 10px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } | |
| .input-section { background: #F9FAFB; border-radius: 12px; padding: 20px; margin: 10px 0; border: 1px solid #E5E7EB; } | |
| .primary-button { background: linear-gradient(45deg, #667eea, #764ba2) !important; color: white !important; border: none !important; border-radius: 10px !important; font-weight: bold !important; } | |
| .success-message { color: #28a745 !important; } | |
| .error-message { color: #dc3545 !important; } | |
| .plot-container { background: white; border-radius: 10px; padding: 15px; margin: 10px 0; box-shadow: 0 5px 15px rgba(0,0,0,0.05); border: 1px solid #E5E7EB; } | |
| .logout-btn { position: fixed !important; top: 15px !important; right: 15px !important; z-index: 1000 !important; } | |
| .report-title h2, .report-title h3 { margin-bottom: 10px; } | |
| /* --- Responsive Summary Cards --- */ | |
| .summary-card { border: 1px solid #E5E7EB; border-radius: 12px; padding: 16px; margin-bottom: 16px; background-color: #F9FAFB; } | |
| .summary-card h3, .summary-card h4 { margin-top: 0; margin-bottom: 12px; color: #1F2937; } | |
| .summary-card ul { list-style-type: none; padding-left: 0; margin: 0; } | |
| .summary-card li { margin-bottom: 8px; color: #4B5563; font-size: 1.05em; } | |
| .summary-card li strong { color: #374151; } | |
| .summary-card.best-choice { background-color: #E0F2FE; border-color: #38BDF8; } | |
| /* --- FIXED: Responsive Design for True Full-Width Mobile View --- */ | |
| @media (max-width: 768px) { | |
| body, .gradio-container { padding: 0 !important; margin: 0 !important; } | |
| .login-container { margin: 0; padding: 25px; border-radius: 0; } | |
| .main-container { margin: 0; padding: 10px; border-radius: 0; box-shadow: none; } | |
| #main-layout { flex-direction: column !important; gap: 20px !important; } | |
| #main-layout > div { width: 100% !important; } | |
| .logout-btn { top: 10px !important; right: 10px !important; } | |
| } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="blue"), title="π Paper Box Optimizer Pro", css=mobile_responsive_css) as app: | |
| with gr.Group(visible=True, elem_classes="login-container") as login_interface: | |
| gr.HTML('<div class="login-title">π SECURE ACCESS PORTAL</div>') | |
| password_input = gr.Textbox(label="Password", type="password", placeholder="Enter your password...") | |
| login_button = gr.Button("π Unlock Optimizer", variant="primary") | |
| login_status = gr.Markdown("", visible=False) | |
| with gr.Group(visible=False, elem_classes="main-container") as main_interface: | |
| logout_button = gr.Button("π Logout", elem_classes="logout-btn") | |
| gr.Markdown("# π Paper Box Production Optimizer") | |
| with gr.Row(elem_id="main-layout"): | |
| with gr.Column(scale=1): | |
| with gr.Group(elem_classes="input-section"): | |
| gr.Markdown("### 1. Material & Production Inputs") | |
| gsm_input = gr.Number(label="Paper GSM", value=250) | |
| price_input = gr.Number(label="Price per KG of Paper", value=90) | |
| box_w_input = gr.Number(label="Box Piece WIDTH (in)", value=9.0) | |
| box_h_input = gr.Number(label="Box Piece HEIGHT (in)", value=16.0) | |
| total_pieces_input = gr.Number(label="Total Pieces to Produce", value=1000, precision=0) | |
| with gr.Group(elem_classes="input-section"): | |
| gr.Markdown("### 2. Sheet Size Selection") | |
| mode_select = gr.Radio(["Auto-Optimize (Standard Sizes)", "Manual (Custom Size)"], label="Choose Analysis Mode", value="Auto-Optimize (Standard Sizes)") | |
| with gr.Group(visible=False) as custom_sheet_inputs: | |
| custom_w_input = gr.Number(label="Custom Sheet WIDTH (in)", value=30) | |
| custom_h_input = gr.Number(label="Custom Sheet HEIGHT (in)", value=36) | |
| run_button = gr.Button("Calculate Optimal Plan", elem_classes="primary-button") | |
| with gr.Column(scale=2): | |
| output_summary = gr.Markdown(label="Analysis & Recommendations") | |
| with gr.Accordion("Show/Hide Visual Layouts", open=True): | |
| # FIXED: Labels are now generic as the title is inside the plot | |
| with gr.Group(elem_classes="plot-container"): | |
| output_plot1 = gr.Plot(label="π₯ Optimal Layout #1") | |
| with gr.Group(elem_classes="plot-container"): | |
| output_plot2 = gr.Plot(label="π₯ Optimal Layout #2") | |
| with gr.Group(elem_classes="plot-container"): | |
| output_plot3 = gr.Plot(label="π₯ Optimal Layout #3") | |
| mode_select.change(fn=update_ui_visibility, inputs=mode_select, outputs=custom_sheet_inputs) | |
| run_button.click( | |
| fn=run_optimizer, | |
| inputs=[gsm_input, price_input, box_w_input, box_h_input, total_pieces_input, mode_select, custom_w_input, custom_h_input], | |
| outputs=[output_summary, output_plot1, output_plot2, output_plot3] | |
| ) | |
| auth_outputs = [login_status, main_interface, login_interface, password_input] | |
| login_button.click(fn=check_password, inputs=password_input, outputs=auth_outputs) | |
| password_input.submit(fn=check_password, inputs=password_input, outputs=auth_outputs) | |
| logout_button.click(fn=logout_user, outputs=[main_interface, login_interface, password_input, login_status]) | |
| if __name__ == "__main__": | |
| app.launch() |