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()