Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import itertools | |
| from scipy.spatial import ConvexHull | |
| import pulp | |
| from fpdf import FPDF | |
| import io | |
| import pandas as pd | |
| from PIL import Image | |
| import tempfile | |
| import os | |
| # ============================================================================== | |
| # 1. FUNÇÕES AUXILIARES | |
| # ============================================================================== | |
| def operador_str(op_norm): | |
| """Converte o operador normalizado (le, ge, eq) para sua representação de string.""" | |
| if op_norm == 'le': | |
| return '<=' | |
| elif op_norm == 'ge': | |
| return '>=' | |
| elif op_norm == 'eq': | |
| return '=' | |
| else: | |
| return op_norm # Fallback | |
| # NOVO: Função para sanitizar texto para compatibilidade com FPDF latin-1 | |
| def sanitize_for_fpdf_latin1(text): | |
| if not isinstance(text, str): | |
| return str(text) # Garante que é uma string | |
| # Substitui caracteres Unicode "smart" (common culprits) por equivalentes ASCII | |
| text = text.replace('\u2019', "'") # Aspas simples direita curvada | |
| text = text.replace('\u201c', '"') # Aspas duplas esquerda curvada | |
| text = text.replace('\u201d', '"') # Aspas duplas direita curvada | |
| text = text.replace('\u2013', '-') # Traço N (en dash) | |
| text = text.replace('\u2014', '--') # Traço M (em dash) | |
| text = text.replace('\u2026', '...') # Reticências | |
| text = text.replace('\u00B0', ' graus') # Símbolo de grau | |
| text = text.replace('\u00B2', '2') # Sobrescrito 2 | |
| # Adicione mais substituições conforme necessário para outros caracteres que possam aparecer | |
| # Fallback para quaisquer outros caracteres não Latin-1 que as substituições acima não cobriram | |
| # Isso os substituirá por '?' ou um equivalente seguro em Latin-1. | |
| try: | |
| text.encode('latin-1') | |
| except UnicodeEncodeError: | |
| text = text.encode('latin-1', errors='replace').decode('latin-1') | |
| return text | |
| def solve_system(eq1, eq2): | |
| """ | |
| Resolve um sistema linear 2x2: | |
| a1*x + b1*y = c1 | |
| a2*x + b2*y = c2 | |
| """ | |
| a1, b1, c1 = eq1 | |
| a2, b2, c2 = eq2 | |
| det = a1*b2 - a2*b1 | |
| if abs(det) < 1e-9: # Linhas paralelas ou idênticas (usar tolerância para floats) | |
| return None # Não há solução única | |
| x = (c1*b2 - c2*b1) / det | |
| y = (a1*c2 - a2*c1) / det | |
| return (x, y) | |
| def is_factible(point, restricoes_originais): | |
| """ | |
| Verifica se um dado ponto (x1, x2) satisfaz todas as restrições e não-negatividade. | |
| """ | |
| x1, x2 = point | |
| # Restrições de não-negatividade (com uma pequena tolerância) | |
| if x1 < -1e-7 or x2 < -1e-7: | |
| return False | |
| # Restrições do problema (com uma pequena tolerância) | |
| for a1, a2, op, b in restricoes_originais: | |
| val = a1*x1 + a2*x2 | |
| if op == 'le' and val > b + 1e-7: # x1+x2 <= b | |
| return False | |
| if op == 'ge' and val < b - 1e-7: # x1+x2 >= b | |
| return False | |
| if op == 'eq' and not np.isclose(val, b, atol=1e-7): # x1+x2 == b | |
| return False | |
| return True | |
| # ============================================================================== | |
| # 2. FUNÇÃO PRINCIPAL DE RESOLUÇÃO GRÁFICA (PL Contínuo) | |
| # ============================================================================== | |
| def resolver_graficamente(c_coeffs, tipo_otimizacao, restricoes_parsed, integer_solution_point=None): | |
| """ | |
| Resolve graficamente um problema de Programação Linear com 2 variáveis. | |
| Retorna o objeto PIL.Image para o Gradio e o BytesIO para o PDF. | |
| """ | |
| # Inclui as restrições de não-negatividade como linhas para encontrar intersecções | |
| all_lines_for_intersections = [(r[0], r[1], r[3]) for r in restricoes_parsed if r[2] != 'eq'] + \ | |
| [(1, 0, 0), (0, 1, 0)] # x1=0, x2=0 (eixos) | |
| vertices = [] | |
| for eq1_coeffs, eq2_coeffs in itertools.combinations(all_lines_for_intersections, 2): | |
| point = solve_system(eq1_coeffs, eq2_coeffs) | |
| if point is not None and is_factible(point, restricoes_parsed): | |
| vertices.append(point) | |
| # Remover duplicatas e pontos muito próximos (tolerância para floats) | |
| vertices_unique = [] | |
| for v in vertices: | |
| if not any(np.allclose(v, uv, atol=1e-7) for uv in vertices_unique): | |
| vertices_unique.append(v) | |
| # Se não houver vértices, a região factível é vazia ou ilimitada | |
| if not vertices_unique: | |
| fig, ax = plt.subplots(figsize=(8, 8)) | |
| if is_factible((0,0), restricoes_parsed): | |
| msg = "Região factível pode ser ilimitada. Não foi possível determinar vértices." | |
| else: | |
| msg = "Região factível vazia. Não há solução contínua." | |
| ax.text(0.5, 0.5, msg, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes, fontsize=12, color='red') | |
| ax.set_title("Status da Região Factível") | |
| ax.set_xlabel("x1") | |
| ax.set_ylabel("x2") | |
| # Salvando figura vazia em BytesIO para retornar para PDF | |
| buf_for_pdf = io.BytesIO() | |
| fig.savefig(buf_for_pdf, format='png', bbox_inches='tight') | |
| plt.close(fig) # Fechar a figura do matplotlib | |
| buf_for_pdf.seek(0) | |
| # Criar uma imagem PIL vazia ou placeholder para o Gradio | |
| img_for_gradio = Image.new('RGB', (600, 600), color = 'white') # Exemplo de imagem vazia | |
| return { | |
| 'funcao_objetivo': f"{tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2", | |
| 'restricoes': restricoes_parsed, | |
| 'regiao_factivel_status': msg, | |
| 'pil_image': img_for_gradio, # Objeto PIL.Image para o Gradio | |
| 'bytes_io_for_pdf': buf_for_pdf, # BytesIO para o PDF | |
| 'vertices_info': [], | |
| 'solucao_otima_vertices': [], | |
| 'valor_otimo_z': None, | |
| 'solucao_tipo_msg': msg | |
| } | |
| # Calcular Z para cada vértice factível | |
| vertices_info = [] | |
| for i, (v1, v2) in enumerate(vertices_unique): | |
| z_val = c_coeffs[0]*v1 + c_coeffs[1]*v2 | |
| vertices_info.append({'nome': f'V{i+1}', 'coordenadas': (round(v1,2), round(v2,2)), 'valor_z': z_val}) | |
| # Encontrar a Solução Ótima Contínua (vértice(s) com o melhor Z) | |
| if tipo_otimizacao == 'maximizar': | |
| best_z = -float('inf') | |
| optimal_vertices_list = [] | |
| else: # minimizar | |
| best_z = float('inf') | |
| optimal_vertices_list = [] | |
| for v_info in vertices_info: | |
| current_z = v_info['valor_z'] | |
| if (tipo_otimizacao == 'maximizar' and current_z > best_z + 1e-7) or \ | |
| (tipo_otimizacao == 'minimizar' and current_z < best_z - 1e-7): | |
| best_z = current_z | |
| optimal_vertices_list = [v_info] | |
| elif np.isclose(current_z, best_z, atol=1e-7): # Empate (com tolerância) | |
| if not any(np.allclose(v_info['coordenadas'], ov['coordenadas'], atol=1e-7) for ov in optimal_vertices_list): | |
| optimal_vertices_list.append(v_info) | |
| if len(optimal_vertices_list) > 1: | |
| solucao_tipo_msg = f"Múltiplas soluções ótimas (nos vértices: {[v['coordenadas'] for v in optimal_vertices_list]} e na aresta entre eles)." | |
| else: | |
| solucao_tipo_msg = "Solução ótima única." | |
| # ========================================================================== | |
| # GERAÇÃO DO GRÁFICO (MATPLOTLIB) | |
| # ========================================================================== | |
| fig, ax = plt.subplots(figsize=(8, 8)) | |
| ax.set_xlabel("x1") | |
| ax.set_ylabel("x2") | |
| ax.set_title(f"Método Gráfico para PL: {tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2") | |
| ax.grid(True) | |
| # Ajustar limites do plot dinamicamente | |
| x_coords_all = [v[0] for v in vertices_unique] + [0] | |
| y_coords_all = [v[1] for v in vertices_unique] + [0] | |
| for a1, a2, op, b in restricoes_parsed: | |
| if a1 != 0: x_coords_all.append(b/a1) | |
| if a2 != 0: y_coords_all.append(b/a2) | |
| if integer_solution_point: | |
| x_coords_all.append(integer_solution_point[0]) | |
| y_coords_all.append(integer_solution_point[1]) | |
| x_min_val = min(x_coords_all) if x_coords_all else 0 | |
| x_max_val = max(x_coords_all) if x_coords_all else 10 | |
| y_min_val = min(y_coords_all) if y_coords_all else 0 | |
| y_max_val = max(y_coords_all) if y_coords_all else 10 | |
| x_lim_min = min(0, x_min_val - 1) | |
| y_lim_min = min(0, y_min_val - 1) | |
| x_lim_max = (x_max_val * 1.2 + 1) if x_max_val > 0 else 10 | |
| y_lim_max = (y_max_val * 1.2 + 1) if y_max_val > 0 else 10 | |
| if x_lim_max < 5: x_lim_max = 5 | |
| if y_lim_max < 5: y_lim_max = 5 | |
| ax.set_xlim(x_lim_min, x_lim_max) | |
| ax.set_ylim(y_lim_min, y_lim_max) | |
| # Plotar as linhas das restrições | |
| for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed): | |
| if abs(a1) < 1e-9 and abs(a2) < 1e-9: continue | |
| x_line = np.linspace(x_lim_min, x_lim_max, 400) | |
| if abs(a1) < 1e-9: | |
| if abs(a2) < 1e-9: continue | |
| y_line = np.full_like(x_line, b / a2) | |
| mask = (y_line >= y_lim_min) & (y_line <= y_lim_max) | |
| ax.plot(x_line[mask], y_line[mask], label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--') | |
| elif abs(a2) < 1e-9: | |
| x_line_val = b / a1 | |
| ax.axvline(x=x_line_val, label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--') | |
| else: | |
| y_line = (b - a1 * x_line) / a2 | |
| mask = (y_line >= y_lim_min) & (y_line <= y_lim_max) | |
| ax.plot(x_line[mask], y_line[mask], label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--') | |
| # Plotar os vértices da região factível | |
| x_vertices_plot = [v[0] for v in vertices_unique] | |
| y_vertices_plot = [v[1] for v in vertices_unique] | |
| ax.plot(x_vertices_plot, y_vertices_plot, 'o', color='blue', markersize=7, label='Vértices Factíveis') | |
| for v_info in vertices_info: | |
| ax.text(v_info['coordenadas'][0]+0.1, v_info['coordenadas'][1]+0.1, v_info['nome'], color='blue', fontsize=9) | |
| # Preencher a região factível usando ConvexHull | |
| if len(vertices_unique) >= 3: | |
| points_np = np.array(vertices_unique) | |
| points_np = points_np[points_np[:,0] >= -1e-7] | |
| points_np = points_np[points_np[:,1] >= -1e-7] | |
| if len(points_np) >= 3: | |
| try: | |
| hull = ConvexHull(points_np) | |
| ordered_hull_points = points_np[hull.vertices] | |
| ax.fill(ordered_hull_points[:,0], ordered_hull_points[:,1], color='green', alpha=0.3, label='Região Factível') | |
| except Exception as e: | |
| print(f"Erro ao calcular ConvexHull para preenchimento: {e}") | |
| # Plotar a função objetivo ótima contínua | |
| best_z_continuous = optimal_vertices_list[0]['valor_z'] if optimal_vertices_list else 0 | |
| if abs(c_coeffs[1]) > 1e-9: | |
| x_z_opt = np.linspace(x_lim_min, x_lim_max, 400) | |
| y_z_opt = (best_z_continuous - c_coeffs[0]*x_z_opt) / c_coeffs[1] | |
| mask = (y_z_opt >= y_lim_min) & (y_z_opt <= y_lim_max) | |
| ax.plot(x_z_opt[mask], y_z_opt[mask], color='red', linewidth=2, label=f'FO Ótima Contínua (Z={best_z_continuous:.2f})') | |
| elif abs(c_coeffs[0]) > 1e-9: | |
| ax.axvline(x=best_z_continuous/c_coeffs[0], color='red', linewidth=2, label=f'FO Ótima Contínua (Z={best_z_continuous:.2f})') | |
| for v_opt_info in optimal_vertices_list: | |
| ax.plot(v_opt_info['coordenadas'][0], v_opt_info['coordenadas'][1], 'X', color='red', markersize=10, | |
| label='Ponto(s) Ótimo(s) Contínuo' if v_opt_info == optimal_vertices_list[0] else "") | |
| # Plotar a Solução Ótima Inteira (se fornecida) | |
| if integer_solution_point: | |
| int_x1, int_x2 = integer_solution_point | |
| int_z_val = c_coeffs[0]*int_x1 + c_coeffs[1]*int_x2 | |
| ax.plot(int_x1, int_x2, 's', color='purple', markersize=10, | |
| label=f'Ponto Ótimo Inteiro (Z={int_z_val:.2f})') | |
| ax.text(int_x1+0.1, int_x2+0.1, f'Z_int={int_z_val:.2f}', color='purple', fontsize=9) | |
| ax.legend(loc='best') | |
| fig.tight_layout() | |
| # Salva a figura em um buffer de BytesIO (para PDF) | |
| buf_for_pdf = io.BytesIO() | |
| fig.savefig(buf_for_pdf, format='png', bbox_inches='tight') | |
| plt.close(fig) # Fechar a figura do matplotlib | |
| buf_for_pdf.seek(0) # Volta ao início do buffer | |
| # Converte o BytesIO para um objeto PIL.Image para Gradio | |
| # É importante criar uma *nova* instância de BytesIO para Image.open, pois Image.open consome o buffer | |
| img_for_gradio = Image.open(io.BytesIO(buf_for_pdf.getvalue())) | |
| return { | |
| 'funcao_objetivo': f"{tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2", | |
| 'restricoes': restricoes_parsed, | |
| 'regiao_factivel_status': 'OK', | |
| 'pil_image': img_for_gradio, # Objeto PIL.Image para o Gradio | |
| 'bytes_io_for_pdf': buf_for_pdf, # BytesIO para o PDF | |
| 'vertices_info': [], | |
| 'solucao_otima_vertices': [v['coordenadas'] for v in optimal_vertices_list], | |
| 'valor_otimo_z': best_z, | |
| 'solucao_tipo_msg': solucao_tipo_msg | |
| } | |
| # ============================================================================== | |
| # 2.1 FUNÇÃO PARA RESOLVER PL (Contínuo ou Inteiro) com PuLP e extrair info | |
| # ============================================================================== | |
| def solve_lp_pulp_unified(c_coeffs, tipo_otimizacao, restricoes_parsed, integer_vars=False): | |
| """ | |
| Resolve um problema de Programação Linear (PL) ou PL Inteira (PLI) usando PuLP. | |
| Retorna a solução ótima (x1, x2), o valor da FO, preços sombra e custos reduzidos. | |
| """ | |
| prob = pulp.LpProblem("Problema_PL", pulp.LpMaximize if tipo_otimizacao == 'maximizar' else pulp.LpMinimize) | |
| # Define variáveis | |
| if integer_vars: | |
| x1 = pulp.LpVariable("x1", lowBound=0, cat='Integer') | |
| x2 = pulp.LpVariable("x2", lowBound=0, cat='Integer') | |
| else: | |
| x1 = pulp.LpVariable("x1", lowBound=0, cat='Continuous') | |
| x2 = pulp.LpVariable("x2", lowBound=0, cat='Continuous') | |
| # Função Objetivo | |
| prob += c_coeffs[0] * x1 + c_coeffs[1] * x2, "Funcao_Objetivo" | |
| # Restrições | |
| for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed): | |
| if op_norm == 'le': | |
| prob += a1 * x1 + a2 * x2 <= b, f"R{i+1}" | |
| elif op_norm == 'ge': | |
| prob += a1 * x1 + a2 * x2 >= b, f"R{i+1}" | |
| elif op_norm == 'eq': | |
| prob += a1 * x1 + a2 * x2 == b, f"R{i+1}" | |
| # Resolver o problema | |
| try: | |
| prob.solve(pulp.PULP_CBC_CMD(msg=0)) # msg=0 para suprimir saída do solver | |
| if prob.status == pulp.LpStatusOptimal: | |
| optimal_point = (pulp.value(x1), pulp.value(x2)) | |
| optimal_z = pulp.value(prob.objective) | |
| reduced_costs = {} | |
| # Custos reduzidos são aplicáveis apenas para PL contínuo e se o solver forneceu | |
| if not integer_vars: | |
| # Verificar se o atributo existe antes de acessar | |
| if hasattr(x1, 'reducedCost') and x1.reducedCost is not None: | |
| reduced_costs['x1'] = x1.reducedCost | |
| if hasattr(x2, 'reducedCost') and x2.reducedCost is not None: | |
| reduced_costs['x2'] = x2.reducedCost | |
| shadow_prices = {} | |
| # Preços sombra são aplicáveis apenas para PL contínuo e se o solver forneceu | |
| if not integer_vars: | |
| for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed): | |
| constraint_name = f"R{i+1}" | |
| # Acessa a restrição pelo nome atribuído | |
| c = prob.constraints[constraint_name] | |
| # Verificar se o atributo existe antes de acessar | |
| if hasattr(c, 'pi') and c.pi is not None: | |
| shadow_prices[constraint_name] = c.pi | |
| return optimal_point, optimal_z, reduced_costs, shadow_prices | |
| else: | |
| return None, None, None, None # Infactível, ilimitado ou outro status | |
| except Exception as e: | |
| print(f"Erro ao resolver LP com PuLP (integer_vars={integer_vars}): {e}") | |
| return None, None, None, None | |
| # ============================================================================== | |
| # 3. FUNÇÃO WRAPPER PARA GRADIO (gradio_solver) | |
| # ============================================================================== | |
| def gradio_solver(question_reference, problem_description, x1_description, x2_description, c1_val, c2_val, opt_type, restrictions_df_input): | |
| # 1. Parsear os coeficientes da função objetivo | |
| try: | |
| c_coeffs = [float(c1_val), float(c2_val)] | |
| except ValueError: | |
| # 11 outputs: output_markdown, output_plot, cont_coords, cont_z, int_coords, int_z, shadow_prices, reduced_costs, status_output, report_data_state, plot_buffer_state | |
| return "Erro: Coeficientes da função objetivo devem ser números válidos.", None, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, io.BytesIO() | |
| # 2. Parsear as restrições do DataFrame | |
| restricoes_parsed = [] | |
| if restrictions_df_input is not None and not restrictions_df_input.empty: | |
| for row_idx, row_series in restrictions_df_input.iterrows(): # Iterar por linhas do DataFrame | |
| row_list = row_series.values.tolist() # Converter a Series da linha para uma lista Python | |
| # Ignorar linhas completamente vazias | |
| if all(val is None or (isinstance(val, str) and val.strip() == '') for val in row_list): | |
| continue | |
| try: | |
| a1 = float(row_list[0]) if row_list[0] is not None else 0.0 | |
| a2 = float(row_list[1]) if row_list[1] is not None else 0.0 | |
| op = str(row_list[2]).lower().strip() if row_list[2] is not None else '' | |
| b = float(row_list[3]) if row_list[3] is not None else 0.0 | |
| # Normalização dos operadores para PuLP e is_factible | |
| op_norm = '' | |
| if op == '<=' or op == '<': op_norm = 'le' | |
| elif op == '>=' or op == '>': op_norm = 'ge' | |
| elif op == '=': op_norm = 'eq' | |
| else: raise ValueError(f"Operador inválido '{op}' na restrição {row_idx+1}. Use '<', '<=', '=', '>=', ou '>'.") | |
| restricoes_parsed.append((a1, a2, op_norm, b)) | |
| except Exception as e: | |
| # 11 outputs | |
| return f"Erro de parsing na restrição {row_idx+1}: {e}. Verifique se todos os campos estão corretos e preenchidos.", None, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, io.BytesIO() | |
| if not restricoes_parsed: | |
| # 11 outputs | |
| return "Erro: Nenhuma restrição válida foi fornecida. Adicione restrições no quadro acima.", None, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, io.BytesIO() | |
| # --- Resolver o problema contínuo com PuLP para extrair preços sombra e custos reduzidos --- | |
| continuous_pulp_point, continuous_pulp_z, reduced_costs, shadow_prices = \ | |
| solve_lp_pulp_unified(c_coeffs, opt_type, restricoes_parsed, integer_vars=False) | |
| # --- Resolver o problema inteiro com PuLP --- | |
| integer_solution_point, integer_optimal_z, _, _ = \ | |
| solve_lp_pulp_unified(c_coeffs, opt_type, restricoes_parsed, integer_vars=True) | |
| # --- Gerar o gráfico FINAL (re-chamar resolver_graficamente com o ponto inteiro, se encontrado) --- | |
| final_plot_result = resolver_graficamente(c_coeffs, opt_type, restricoes_parsed, integer_solution_point=integer_solution_point) | |
| # --- Preparar as saídas para o Gradio --- | |
| output_text = f"## Resolução do Problema de PL\n\n" \ | |
| f"**Função Objetivo:** {final_plot_result['funcao_objetivo']}\n" \ | |
| f"**Status da Região Factível (Contínua):** {final_plot_result['regiao_factivel_status']}\n" | |
| continuous_coords_output_str = "N/A" | |
| continuous_z_output_str = "N/A" | |
| integer_coords_output_str = "N/A" | |
| integer_z_output_str = "N/A" | |
| integer_sol_info = "" | |
| shadow_prices_output_str = "N/A" | |
| reduced_costs_output_str = "N/A" | |
| if final_plot_result['regiao_factivel_status'] == 'OK': | |
| output_text += f"\n**Vértices da Região Factível (Contínua):**\n" | |
| for v_info in final_plot_result['vertices_info']: | |
| output_text += f"- {v_info['nome']}: ({v_info['coordenadas'][0]:.2f}, {v_info['coordenadas'][1]:.2f}) -> Z = {v_info['valor_z']:.2f}\n" | |
| output_text += f"\n**Solução Ótima Contínua:** {final_plot_result['solucao_tipo_msg']}\n" | |
| if continuous_pulp_point is not None: | |
| output_text += f"**Ponto(s) Ótimo(s) Contínuo:** ({continuous_pulp_point[0]:.2f}, {continuous_pulp_point[1]:.2f})\n" \ | |
| f"**Valor Ótimo Contínuo de Z:** {continuous_pulp_z:.2f}\n" | |
| continuous_coords_output_str = f"({continuous_pulp_point[0]:.2f}, {continuous_pulp_point[1]:.2f})" | |
| continuous_z_output_str = f"{continuous_pulp_z:.2f}" | |
| else: | |
| output_text += f"Não foi possível determinar a solução contínua pelo PuLP.\n" | |
| # Formatar Preços Sombra e Custos Reduzidos | |
| if shadow_prices: | |
| shadow_prices_str = "\n".join([f" - {k}: {v:.4f}" for k, v in shadow_prices.items()]) | |
| output_text += f"\n**Preços Sombra das Restrições:**\n{shadow_prices_str}\n" | |
| shadow_prices_output_str = shadow_prices_str | |
| else: | |
| output_text += f"\n**Preços Sombra das Restrições:** N/A (Não calculados ou problema infactível/ilimitado)\n" | |
| if reduced_costs: | |
| reduced_costs_str = "\n".join([f" - {k}: {v:.4f}" for k, v in reduced_costs.items()]) | |
| output_text += f"\n**Custos Reduzidos das Variáveis:**\n{reduced_costs_str}\n" | |
| reduced_costs_output_str = reduced_costs_str | |
| else: | |
| output_text += f"\n**Custos Reduzidos das Variáveis:** N/A (Não calculados ou problema infactível/ilimitado)\n" | |
| if integer_solution_point is not None: | |
| integer_coords_output_str = f"({integer_solution_point[0]:.0f}, {integer_solution_point[1]:.0f})" | |
| integer_z_output_str = f"{integer_optimal_z:.2f}" | |
| solution_coincides = False | |
| if continuous_pulp_point is not None and \ | |
| np.isclose(continuous_pulp_point[0], integer_solution_point[0], atol=1e-6) and \ | |
| np.isclose(continuous_pulp_point[1], integer_solution_point[1], atol=1e-6) and \ | |
| np.isclose(continuous_pulp_z, integer_optimal_z, atol=1e-2): | |
| solution_coincides = True | |
| if solution_coincides: | |
| integer_sol_info += f"A solução ótima inteira ({integer_coords_output_str}) **coincide** com a solução contínua, com Z = {integer_z_output_str}.\n" | |
| else: | |
| integer_sol_info += f"**Ponto Ótimo Inteiro:** {integer_coords_output_str}\n" \ | |
| f"**Valor Ótimo Inteiro de Z:** {integer_z_output_str}\n" | |
| output_text += f"\n**Solução Ótima Inteira:**\n" + integer_sol_info | |
| else: | |
| integer_sol_info = "Não foi encontrada uma solução inteira factível ou o problema é infactível para inteiros." | |
| output_text += f"\n**Solução Ótima Inteira:** {integer_sol_info}\n" | |
| else: | |
| integer_sol_info = "Não aplicável, pois a região factível contínua não existe ou é ilimitada." | |
| output_text += f"\n**Solução Ótima Inteira:** {integer_sol_info}\n" | |
| # Preparar dados para o relatório PDF | |
| report_data_for_pdf = { | |
| 'question_reference': question_reference, | |
| 'problem_description': problem_description, | |
| 'x1_description': x1_description, | |
| 'x2_description': x2_description, | |
| 'output_markdown': output_text, | |
| 'shadow_prices': shadow_prices, | |
| 'reduced_costs': reduced_costs, | |
| } | |
| return output_text, final_plot_result['pil_image'], \ | |
| continuous_coords_output_str, \ | |
| continuous_z_output_str, \ | |
| integer_coords_output_str, \ | |
| integer_z_output_str, \ | |
| shadow_prices_output_str, \ | |
| reduced_costs_output_str, \ | |
| final_plot_result['regiao_factivel_status'], \ | |
| report_data_for_pdf, \ | |
| final_plot_result['bytes_io_for_pdf'] # Retorna bytes_io_for_pdf para o plot_buffer_state | |
| # ============================================================================== | |
| # 3.1 FUNÇÃO PARA GERAR RELATÓRIO PDF | |
| # ============================================================================== | |
| class PDF(FPDF): | |
| def __init__(self, orientation='P', unit='mm', format='A4', question_reference_raw="Relatório de PL"): | |
| super().__init__(orientation, unit, format) | |
| self.question_reference = sanitize_for_fpdf_latin1(question_reference_raw) # Sanitizar aqui | |
| def header(self): | |
| self.set_font('Arial', 'B', 15) | |
| self.cell(0, 10, self.question_reference, 0, 1, 'C') # Usa a referência como título | |
| self.ln(10) | |
| def footer(self): | |
| self.set_y(-15) | |
| self.set_font('Arial', 'I', 8) | |
| self.cell(0, 10, f'Página {self.page_no()}/{{nb}}', 0, 0, 'C') | |
| def generate_pdf(report_data, plot_figure_io_for_pdf): | |
| tmp_png_path = None | |
| tmp_pdf_path = None | |
| try: | |
| # Passa a referência da questão para o construtor do PDF | |
| pdf = PDF(question_reference_raw=report_data.get('question_reference', 'Relatório de PL')) | |
| pdf.alias_nb_pages() | |
| pdf.add_page() | |
| pdf.set_font('Arial', '', 12) | |
| # Adicionar descrição do problema (NOVO) | |
| if report_data.get('problem_description'): | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.multi_cell(0, 7, 'Descrição do Problema:') | |
| pdf.set_font('Arial', '', 10) | |
| pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['problem_description'])) # Sanitizar aqui | |
| pdf.ln(5) | |
| # Adicionar descrição de X1 (NOVO) | |
| if report_data.get('x1_description'): | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.multi_cell(0, 7, 'Variável X1:') | |
| pdf.set_font('Arial', '', 10) | |
| pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['x1_description'])) # Sanitizar aqui | |
| pdf.ln(5) | |
| # Adicionar descrição de X2 (NOVO) | |
| if report_data.get('x2_description'): | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.multi_cell(0, 7, 'Variável X2:') | |
| pdf.set_font('Arial', '', 10) | |
| pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['x2_description'])) # Sanitizar aqui | |
| pdf.ln(5) | |
| # Adicionar o conteúdo markdown principal da solução | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.multi_cell(0, 7, 'Análise da Solução:') | |
| pdf.set_font('Arial', '', 10) | |
| formatted_text = report_data['output_markdown'].replace('##', '').replace('**', '').replace('\n', '\n').strip() | |
| pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(formatted_text)) # Sanitizar aqui | |
| pdf.ln(5) | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.multi_cell(0, 7, 'Detalhes Adicionais:') | |
| pdf.set_font('Arial', '', 10) | |
| # Adicionar Preços Sombra | |
| if 'shadow_prices' in report_data and report_data['shadow_prices']: | |
| pdf.multi_cell(0, 7, 'Preços Sombra (Variáveis Duais):') | |
| for k, v in report_data['shadow_prices'].items(): | |
| pdf.multi_cell(0, 5, sanitize_for_fpdf_latin1(f'- {k}: {v:.4f}')) # Sanitizar aqui | |
| else: | |
| pdf.multi_cell(0, 7, 'Preços Sombra: N/A') | |
| # Adicionar Custos Reduzidos | |
| if 'reduced_costs' in report_data and report_data['reduced_costs']: | |
| pdf.multi_cell(0, 7, 'Custos Reduzidos (Variáveis Primal):') | |
| for k, v in report_data['reduced_costs'].items(): | |
| pdf.multi_cell(0, 5, sanitize_for_fpdf_latin1(f'- {k}: {v:.4f}')) # Sanitizar aqui | |
| else: | |
| pdf.multi_cell(0, 7, 'Custos Reduzidos: N/A') | |
| # Adicionar gráfico | |
| if plot_figure_io_for_pdf and plot_figure_io_for_pdf.getbuffer().nbytes > 0: | |
| pdf.add_page() | |
| pdf.set_font('Arial', 'B', 12) | |
| pdf.cell(0, 10, 'Gráfico da Solução:', 0, 1, 'L') | |
| plot_figure_io_for_pdf.seek(0) | |
| # Criar um arquivo temporário para a imagem PNG | |
| with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file: | |
| tmp_file.write(plot_figure_io_for_pdf.getvalue()) | |
| tmp_png_path = tmp_file.name | |
| pdf.image(tmp_png_path, x=10, y=pdf.get_y(), w=180) | |
| # Gerar PDF bytes | |
| pdf_string_output = pdf.output(dest='S') | |
| pdf_bytes_output = pdf_string_output.encode('latin-1') | |
| # Salvar PDF bytes para um arquivo temporário | |
| with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_pdf_file: | |
| tmp_pdf_file.write(pdf_bytes_output) | |
| tmp_pdf_path = tmp_pdf_file.name | |
| # Retorna o PATH para o arquivo PDF temporário. Gradio irá gerenciar o download e a limpeza. | |
| return gr.update(value=tmp_pdf_path, label="Download Relatório PDF", visible=True) | |
| finally: | |
| # Garante que o arquivo PNG temporário seja excluído, se foi criado | |
| if tmp_png_path and os.path.exists(tmp_png_path): | |
| os.remove(tmp_png_path) | |
| # Não excluímos tmp_pdf_path aqui, pois Gradio precisa dele para download e deve limpá-lo. | |
| # ============================================================================== | |
| # 4. DEFINIÇÃO E LANÇAMENTO DA INTERFACE GRADIO (usando gr.Blocks) | |
| # ============================================================================== | |
| with gr.Blocks(title="Resolvedor LP Gráfico (2 Variáveis) com Solução Inteira") as demo: | |
| gr.Markdown("# Universidade de Brasília – UnB") | |
| gr.Markdown("### Programa de Pós-graduação em Computação Aplicada – PPCA") | |
| gr.Markdown("## Mestrado Profissional") | |
| gr.Markdown("### Fundamentos em Pesquisa Operacional – 2025/2") | |
| gr.Markdown("#### Professor: Peng Yaohao") | |
| gr.Markdown("#### Alunos: Douglas Lopes dos Santos, Éder Marcelo P. Cunha, Gilson Araújo e Pedro Britto Junior") | |
| gr.Markdown("---") | |
| gr.Markdown("Este aplicativo resolve problemas de Programação Linear (PL) com duas variáveis de decisão, calcula a solução ótima inteira e as exibe no gráfico.") | |
| gr.Markdown("> **Nota sobre desigualdades estritas:** Para fins de solução via PuLP e representação gráfica, desigualdades estritas (como `<` ou `>`) são tratadas como suas versões não-estritas (i.e., `x < 5` é modelado como `x <= 5`, e `x > 5` como `x >= 6` para inteiros). Em PL contínua, a otimalidade geralmente ocorre nos vértices do polígono, e a fronteira é incluída na região factível. Para variáveis inteiras, `x < N` seria `x <= N-1`.") | |
| gr.Markdown("---") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 1. Detalhes do Problema") | |
| question_reference_input = gr.Textbox(label="Referência da Questão/Trabalho", value="Trabalho Final FPO", placeholder="Ex: Questão 1a - Produção de Móveis") | |
| problem_description_input = gr.Textbox(label="Descrição Detalhada do Problema", lines=3, placeholder="Descreva aqui o problema que está sendo modelado, os objetivos e o contexto...", value="") | |
| x1_description_input = gr.Textbox(label="O que é X1?", placeholder="Ex: Quantidade de produto A em unidades", value="") | |
| x2_description_input = gr.Textbox(label="O que é X2?", placeholder="Ex: Quantidade de produto B em unidades", value="") | |
| c1_input = gr.Number(label="Coeficiente c1 (para x1)", value=2.0) | |
| c2_input = gr.Number(label="Coeficiente c2 (para x2)", value=3.0) | |
| opt_type_radio = gr.Radio(["maximizar", "minimizar"], label="Tipo de Otimização", value="maximizar") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 2. Restrições") | |
| default_restrictions = [[1, 1, "<=", 5], [2, 1, "<=", 8]] | |
| restrictions_dataframe = gr.Dataframe( | |
| headers=["x1 Coef", "x2 Coef", "Operador", "RHS"], | |
| # OPERADORES RESTRITOS AGORA | |
| datatype=["number", "number", {"choices": [">", ">=", "=", "<", "<="], "type": "str"}, "number"], | |
| value=default_restrictions, | |
| row_count=(len(default_restrictions), "dynamic"), | |
| column_count=(4, "fixed"), | |
| label="Defina as Restrições. Linhas vazias ou incompletas serão ignoradas.", | |
| interactive=True | |
| ) | |
| add_row_btn = gr.Button("Adicionar Linha de Restrição", size="sm") | |
| def add_restriction_row_to_df(current_df_value: pd.DataFrame): | |
| data_as_list = [] | |
| if current_df_value is not None and not current_df_value.empty: | |
| data_as_list = current_df_value.values.tolist() | |
| new_row = [None, None, "<=", None] | |
| data_as_list.append(new_row) | |
| return gr.update(value=data_as_list, row_count=(len(data_as_list), "dynamic")) | |
| add_row_btn.click( | |
| fn=add_restriction_row_to_df, | |
| inputs=[restrictions_dataframe], | |
| outputs=[restrictions_dataframe] | |
| ) | |
| with gr.Row(): | |
| solve_btn = gr.Button("3. Resolver Problema", variant="primary", size="lg") | |
| clear_btn = gr.Button("Limpar Campos", variant="secondary", size="lg") | |
| gr.Markdown("---") # Separador visual | |
| output_markdown = gr.Markdown(label="Detalhes da Resolução") | |
| output_plot = gr.Image(label="Gráfico da Região Factível, Solução Contínua e Solução Inteira", type="pil", width=600, height=600) | |
| with gr.Row(): | |
| continuous_coords_output = gr.Textbox(label="Ponto(s) Ótimo(s) Contínuo", interactive=False) | |
| continuous_z_output = gr.Textbox(label="Valor Ótimo Contínuo de Z", interactive=False) | |
| with gr.Row(): | |
| integer_coords_output = gr.Textbox(label="Ponto Ótimo Inteiro", interactive=False) | |
| integer_z_output = gr.Textbox(label="Valor Ótimo Inteiro de Z", interactive=False) | |
| with gr.Row(): | |
| shadow_prices_output = gr.Textbox(label="Preços Sombra (Restrições)", lines=3, interactive=False) | |
| reduced_costs_output = gr.Textbox(label="Custos Reduzidos (Variáveis)", lines=3, interactive=False) | |
| status_output = gr.Textbox(label="Status da Região Factível (Contínua)", interactive=False) | |
| report_data_state = gr.State(value={}) # Para armazenar dados para o PDF | |
| plot_buffer_state = gr.State(value=io.BytesIO()) | |
| pdf_download_btn = gr.Button("Gerar Relatório PDF", variant="secondary") | |
| # O gr.File agora será do tipo "filepath" e receberá um caminho de arquivo | |
| pdf_output_file = gr.File(label="Relatório PDF", file_count="single", interactive=False, visible=False, type="filepath") | |
| # Define o manipulador de eventos para o botão Resolver | |
| solve_btn.click( | |
| fn=gradio_solver, | |
| inputs=[ | |
| question_reference_input, problem_description_input, x1_description_input, x2_description_input, # Novos inputs | |
| c1_input, c2_input, opt_type_radio, restrictions_dataframe | |
| ], | |
| outputs=[ | |
| output_markdown, output_plot, | |
| continuous_coords_output, continuous_z_output, | |
| integer_coords_output, integer_z_output, | |
| shadow_prices_output, reduced_costs_output, | |
| status_output, report_data_state, plot_buffer_state | |
| ] | |
| ) | |
| # Manipulador de eventos para o botão de PDF | |
| pdf_download_btn.click( | |
| fn=generate_pdf, | |
| inputs=[report_data_state, plot_buffer_state], # Pega os dados do estado e o buffer do plot | |
| outputs=[pdf_output_file] | |
| ) | |
| # Função para limpar todos os campos | |
| def clear_all_inputs(): | |
| return ( | |
| gr.update(value="Trabalho Final FPO"), # question_reference_input | |
| gr.update(value=""), # problem_description_input | |
| gr.update(value=""), # x1_description_input | |
| gr.update(value=""), # x2_description_input | |
| gr.update(value=2.0), # c1_input | |
| gr.update(value=3.0), # c2_input | |
| gr.update(value="maximizar"), # opt_type_radio | |
| gr.update(value=[[1, 1, "<=", 5], [2, 1, "<=", 8]]), # restrictions_dataframe | |
| gr.update(value=""), # output_markdown | |
| gr.update(value=None), # output_plot (limpa a imagem) | |
| gr.update(value="N/A"), # continuous_coords_output | |
| gr.update(value="N/A"), # continuous_z_output | |
| gr.update(value="N/A"), # integer_coords_output | |
| gr.update(value="N/A"), # integer_z_output | |
| gr.update(value="N/A"), # shadow_prices_output | |
| gr.update(value="N/A"), # reduced_costs_output | |
| gr.update(value=""), # status_output | |
| {}, # report_data_state (reset state) | |
| io.BytesIO(), # plot_buffer_state (reset state) | |
| gr.File(label="Relatório PDF", file_count="single", interactive=False, visible=False, type="filepath") # pdf_output_file | |
| ) | |
| clear_btn.click( | |
| fn=clear_all_inputs, | |
| outputs=[ | |
| question_reference_input, problem_description_input, x1_description_input, x2_description_input, | |
| c1_input, c2_input, opt_type_radio, restrictions_dataframe, | |
| output_markdown, output_plot, | |
| continuous_coords_output, continuous_z_output, | |
| integer_coords_output, integer_z_output, | |
| shadow_prices_output, reduced_costs_output, | |
| status_output, report_data_state, plot_buffer_state, | |
| pdf_output_file | |
| ] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |