252106862eder commited on
Commit
db306bd
·
verified ·
1 Parent(s): 7147d76

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +785 -0
app.py ADDED
@@ -0,0 +1,785 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import itertools
5
+ from scipy.spatial import ConvexHull
6
+ import pulp
7
+ from fpdf import FPDF
8
+ import io
9
+ import pandas as pd
10
+ from PIL import Image
11
+ import tempfile
12
+ import os
13
+
14
+ # ==============================================================================
15
+ # 1. FUNÇÕES AUXILIARES
16
+ # ==============================================================================
17
+
18
+ def operador_str(op_norm):
19
+ """Converte o operador normalizado (le, ge, eq) para sua representação de string."""
20
+ if op_norm == 'le':
21
+ return '<='
22
+ elif op_norm == 'ge':
23
+ return '>='
24
+ elif op_norm == 'eq':
25
+ return '='
26
+ else:
27
+ return op_norm # Fallback
28
+
29
+ # NOVO: Função para sanitizar texto para compatibilidade com FPDF latin-1
30
+ def sanitize_for_fpdf_latin1(text):
31
+ if not isinstance(text, str):
32
+ return str(text) # Garante que é uma string
33
+
34
+ # Substitui caracteres Unicode "smart" (common culprits) por equivalentes ASCII
35
+ text = text.replace('\u2019', "'") # Aspas simples direita curvada
36
+ text = text.replace('\u201c', '"') # Aspas duplas esquerda curvada
37
+ text = text.replace('\u201d', '"') # Aspas duplas direita curvada
38
+ text = text.replace('\u2013', '-') # Traço N (en dash)
39
+ text = text.replace('\u2014', '--') # Traço M (em dash)
40
+ text = text.replace('\u2026', '...') # Reticências
41
+ text = text.replace('\u00B0', ' graus') # Símbolo de grau
42
+ text = text.replace('\u00B2', '2') # Sobrescrito 2
43
+ # Adicione mais substituições conforme necessário para outros caracteres que possam aparecer
44
+
45
+ # Fallback para quaisquer outros caracteres não Latin-1 que as substituições acima não cobriram
46
+ # Isso os substituirá por '?' ou um equivalente seguro em Latin-1.
47
+ try:
48
+ text.encode('latin-1')
49
+ except UnicodeEncodeError:
50
+ text = text.encode('latin-1', errors='replace').decode('latin-1')
51
+
52
+ return text
53
+
54
+
55
+ def solve_system(eq1, eq2):
56
+ """
57
+ Resolve um sistema linear 2x2:
58
+ a1*x + b1*y = c1
59
+ a2*x + b2*y = c2
60
+ """
61
+ a1, b1, c1 = eq1
62
+ a2, b2, c2 = eq2
63
+
64
+ det = a1*b2 - a2*b1
65
+ if abs(det) < 1e-9: # Linhas paralelas ou idênticas (usar tolerância para floats)
66
+ return None # Não há solução única
67
+
68
+ x = (c1*b2 - c2*b1) / det
69
+ y = (a1*c2 - a2*c1) / det
70
+ return (x, y)
71
+
72
+ def is_factible(point, restricoes_originais):
73
+ """
74
+ Verifica se um dado ponto (x1, x2) satisfaz todas as restrições e não-negatividade.
75
+ """
76
+ x1, x2 = point
77
+
78
+ # Restrições de não-negatividade (com uma pequena tolerância)
79
+ if x1 < -1e-7 or x2 < -1e-7:
80
+ return False
81
+
82
+ # Restrições do problema (com uma pequena tolerância)
83
+ for a1, a2, op, b in restricoes_originais:
84
+ val = a1*x1 + a2*x2
85
+ if op == 'le' and val > b + 1e-7: # x1+x2 <= b
86
+ return False
87
+ if op == 'ge' and val < b - 1e-7: # x1+x2 >= b
88
+ return False
89
+ if op == 'eq' and not np.isclose(val, b, atol=1e-7): # x1+x2 == b
90
+ return False
91
+ return True
92
+
93
+ # ==============================================================================
94
+ # 2. FUNÇÃO PRINCIPAL DE RESOLUÇÃO GRÁFICA (PL Contínuo)
95
+ # ==============================================================================
96
+
97
+ def resolver_graficamente(c_coeffs, tipo_otimizacao, restricoes_parsed, integer_solution_point=None):
98
+ """
99
+ Resolve graficamente um problema de Programação Linear com 2 variáveis.
100
+ Retorna o objeto PIL.Image para o Gradio e o BytesIO para o PDF.
101
+ """
102
+
103
+ # Inclui as restrições de não-negatividade como linhas para encontrar intersecções
104
+ all_lines_for_intersections = [(r[0], r[1], r[3]) for r in restricoes_parsed if r[2] != 'eq'] + \
105
+ [(1, 0, 0), (0, 1, 0)] # x1=0, x2=0 (eixos)
106
+
107
+ vertices = []
108
+ for eq1_coeffs, eq2_coeffs in itertools.combinations(all_lines_for_intersections, 2):
109
+ point = solve_system(eq1_coeffs, eq2_coeffs)
110
+ if point is not None and is_factible(point, restricoes_parsed):
111
+ vertices.append(point)
112
+
113
+ # Remover duplicatas e pontos muito próximos (tolerância para floats)
114
+ vertices_unique = []
115
+ for v in vertices:
116
+ if not any(np.allclose(v, uv, atol=1e-7) for uv in vertices_unique):
117
+ vertices_unique.append(v)
118
+
119
+ # Se não houver vértices, a região factível é vazia ou ilimitada
120
+ if not vertices_unique:
121
+ fig, ax = plt.subplots(figsize=(8, 8))
122
+ if is_factible((0,0), restricoes_parsed):
123
+ msg = "Região factível pode ser ilimitada. Não foi possível determinar vértices."
124
+ else:
125
+ msg = "Região factível vazia. Não há solução contínua."
126
+
127
+ ax.text(0.5, 0.5, msg, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes, fontsize=12, color='red')
128
+ ax.set_title("Status da Região Factível")
129
+ ax.set_xlabel("x1")
130
+ ax.set_ylabel("x2")
131
+
132
+ # Salvando figura vazia em BytesIO para retornar para PDF
133
+ buf_for_pdf = io.BytesIO()
134
+ fig.savefig(buf_for_pdf, format='png', bbox_inches='tight')
135
+ plt.close(fig) # Fechar a figura do matplotlib
136
+ buf_for_pdf.seek(0)
137
+
138
+ # Criar uma imagem PIL vazia ou placeholder para o Gradio
139
+ img_for_gradio = Image.new('RGB', (600, 600), color = 'white') # Exemplo de imagem vazia
140
+
141
+ return {
142
+ 'funcao_objetivo': f"{tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2",
143
+ 'restricoes': restricoes_parsed,
144
+ 'regiao_factivel_status': msg,
145
+ 'pil_image': img_for_gradio, # Objeto PIL.Image para o Gradio
146
+ 'bytes_io_for_pdf': buf_for_pdf, # BytesIO para o PDF
147
+ 'vertices_info': [],
148
+ 'solucao_otima_vertices': [],
149
+ 'valor_otimo_z': None,
150
+ 'solucao_tipo_msg': msg
151
+ }
152
+
153
+ # Calcular Z para cada vértice factível
154
+ vertices_info = []
155
+ for i, (v1, v2) in enumerate(vertices_unique):
156
+ z_val = c_coeffs[0]*v1 + c_coeffs[1]*v2
157
+ vertices_info.append({'nome': f'V{i+1}', 'coordenadas': (round(v1,2), round(v2,2)), 'valor_z': z_val})
158
+
159
+ # Encontrar a Solução Ótima Contínua (vértice(s) com o melhor Z)
160
+ if tipo_otimizacao == 'maximizar':
161
+ best_z = -float('inf')
162
+ optimal_vertices_list = []
163
+ else: # minimizar
164
+ best_z = float('inf')
165
+ optimal_vertices_list = []
166
+
167
+ for v_info in vertices_info:
168
+ current_z = v_info['valor_z']
169
+ if (tipo_otimizacao == 'maximizar' and current_z > best_z + 1e-7) or \
170
+ (tipo_otimizacao == 'minimizar' and current_z < best_z - 1e-7):
171
+ best_z = current_z
172
+ optimal_vertices_list = [v_info]
173
+ elif np.isclose(current_z, best_z, atol=1e-7): # Empate (com tolerância)
174
+ if not any(np.allclose(v_info['coordenadas'], ov['coordenadas'], atol=1e-7) for ov in optimal_vertices_list):
175
+ optimal_vertices_list.append(v_info)
176
+
177
+ if len(optimal_vertices_list) > 1:
178
+ 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)."
179
+ else:
180
+ solucao_tipo_msg = "Solução ótima única."
181
+
182
+ # ==========================================================================
183
+ # GERAÇÃO DO GRÁFICO (MATPLOTLIB)
184
+ # ==========================================================================
185
+ fig, ax = plt.subplots(figsize=(8, 8))
186
+ ax.set_xlabel("x1")
187
+ ax.set_ylabel("x2")
188
+ ax.set_title(f"Método Gráfico para PL: {tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2")
189
+ ax.grid(True)
190
+
191
+ # Ajustar limites do plot dinamicamente
192
+ x_coords_all = [v[0] for v in vertices_unique] + [0]
193
+ y_coords_all = [v[1] for v in vertices_unique] + [0]
194
+ for a1, a2, op, b in restricoes_parsed:
195
+ if a1 != 0: x_coords_all.append(b/a1)
196
+ if a2 != 0: y_coords_all.append(b/a2)
197
+
198
+ if integer_solution_point:
199
+ x_coords_all.append(integer_solution_point[0])
200
+ y_coords_all.append(integer_solution_point[1])
201
+
202
+ x_min_val = min(x_coords_all) if x_coords_all else 0
203
+ x_max_val = max(x_coords_all) if x_coords_all else 10
204
+ y_min_val = min(y_coords_all) if y_coords_all else 0
205
+ y_max_val = max(y_coords_all) if y_coords_all else 10
206
+
207
+ x_lim_min = min(0, x_min_val - 1)
208
+ y_lim_min = min(0, y_min_val - 1)
209
+ x_lim_max = (x_max_val * 1.2 + 1) if x_max_val > 0 else 10
210
+ y_lim_max = (y_max_val * 1.2 + 1) if y_max_val > 0 else 10
211
+
212
+ if x_lim_max < 5: x_lim_max = 5
213
+ if y_lim_max < 5: y_lim_max = 5
214
+
215
+ ax.set_xlim(x_lim_min, x_lim_max)
216
+ ax.set_ylim(y_lim_min, y_lim_max)
217
+
218
+ # Plotar as linhas das restrições
219
+ for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed):
220
+ if abs(a1) < 1e-9 and abs(a2) < 1e-9: continue
221
+
222
+ x_line = np.linspace(x_lim_min, x_lim_max, 400)
223
+ if abs(a1) < 1e-9:
224
+ if abs(a2) < 1e-9: continue
225
+ y_line = np.full_like(x_line, b / a2)
226
+ mask = (y_line >= y_lim_min) & (y_line <= y_lim_max)
227
+ ax.plot(x_line[mask], y_line[mask], label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--')
228
+ elif abs(a2) < 1e-9:
229
+ x_line_val = b / a1
230
+ ax.axvline(x=x_line_val, label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--')
231
+ else:
232
+ y_line = (b - a1 * x_line) / a2
233
+ mask = (y_line >= y_lim_min) & (y_line <= y_lim_max)
234
+ ax.plot(x_line[mask], y_line[mask], label=f'R{i+1}: {a1}x1 + {a2}x2 {operador_str(op_norm)} {b}', linestyle='--')
235
+
236
+ # Plotar os vértices da região factível
237
+ x_vertices_plot = [v[0] for v in vertices_unique]
238
+ y_vertices_plot = [v[1] for v in vertices_unique]
239
+ ax.plot(x_vertices_plot, y_vertices_plot, 'o', color='blue', markersize=7, label='Vértices Factíveis')
240
+ for v_info in vertices_info:
241
+ ax.text(v_info['coordenadas'][0]+0.1, v_info['coordenadas'][1]+0.1, v_info['nome'], color='blue', fontsize=9)
242
+
243
+ # Preencher a região factível usando ConvexHull
244
+ if len(vertices_unique) >= 3:
245
+ points_np = np.array(vertices_unique)
246
+ points_np = points_np[points_np[:,0] >= -1e-7]
247
+ points_np = points_np[points_np[:,1] >= -1e-7]
248
+
249
+ if len(points_np) >= 3:
250
+ try:
251
+ hull = ConvexHull(points_np)
252
+ ordered_hull_points = points_np[hull.vertices]
253
+ ax.fill(ordered_hull_points[:,0], ordered_hull_points[:,1], color='green', alpha=0.3, label='Região Factível')
254
+ except Exception as e:
255
+ print(f"Erro ao calcular ConvexHull para preenchimento: {e}")
256
+
257
+ # Plotar a função objetivo ótima contínua
258
+ best_z_continuous = optimal_vertices_list[0]['valor_z'] if optimal_vertices_list else 0
259
+
260
+ if abs(c_coeffs[1]) > 1e-9:
261
+ x_z_opt = np.linspace(x_lim_min, x_lim_max, 400)
262
+ y_z_opt = (best_z_continuous - c_coeffs[0]*x_z_opt) / c_coeffs[1]
263
+ mask = (y_z_opt >= y_lim_min) & (y_z_opt <= y_lim_max)
264
+ 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})')
265
+ elif abs(c_coeffs[0]) > 1e-9:
266
+ ax.axvline(x=best_z_continuous/c_coeffs[0], color='red', linewidth=2, label=f'FO Ótima Contínua (Z={best_z_continuous:.2f})')
267
+
268
+ for v_opt_info in optimal_vertices_list:
269
+ ax.plot(v_opt_info['coordenadas'][0], v_opt_info['coordenadas'][1], 'X', color='red', markersize=10,
270
+ label='Ponto(s) Ótimo(s) Contínuo' if v_opt_info == optimal_vertices_list[0] else "")
271
+
272
+ # Plotar a Solução Ótima Inteira (se fornecida)
273
+ if integer_solution_point:
274
+ int_x1, int_x2 = integer_solution_point
275
+ int_z_val = c_coeffs[0]*int_x1 + c_coeffs[1]*int_x2
276
+ ax.plot(int_x1, int_x2, 's', color='purple', markersize=10,
277
+ label=f'Ponto Ótimo Inteiro (Z={int_z_val:.2f})')
278
+ ax.text(int_x1+0.1, int_x2+0.1, f'Z_int={int_z_val:.2f}', color='purple', fontsize=9)
279
+
280
+ ax.legend(loc='best')
281
+ fig.tight_layout()
282
+
283
+ # Salva a figura em um buffer de BytesIO (para PDF)
284
+ buf_for_pdf = io.BytesIO()
285
+ fig.savefig(buf_for_pdf, format='png', bbox_inches='tight')
286
+ plt.close(fig) # Fechar a figura do matplotlib
287
+ buf_for_pdf.seek(0) # Volta ao início do buffer
288
+
289
+ # Converte o BytesIO para um objeto PIL.Image para Gradio
290
+ # É importante criar uma *nova* instância de BytesIO para Image.open, pois Image.open consome o buffer
291
+ img_for_gradio = Image.open(io.BytesIO(buf_for_pdf.getvalue()))
292
+
293
+ return {
294
+ 'funcao_objetivo': f"{tipo_otimizacao.capitalize()} Z = {c_coeffs[0]}x1 + {c_coeffs[1]}x2",
295
+ 'restricoes': restricoes_parsed,
296
+ 'regiao_factivel_status': 'OK',
297
+ 'pil_image': img_for_gradio, # Objeto PIL.Image para o Gradio
298
+ 'bytes_io_for_pdf': buf_for_pdf, # BytesIO para o PDF
299
+ 'vertices_info': [],
300
+ 'solucao_otima_vertices': [v['coordenadas'] for v in optimal_vertices_list],
301
+ 'valor_otimo_z': best_z,
302
+ 'solucao_tipo_msg': solucao_tipo_msg
303
+ }
304
+
305
+ # ==============================================================================
306
+ # 2.1 FUNÇÃO PARA RESOLVER PL (Contínuo ou Inteiro) com PuLP e extrair info
307
+ # ==============================================================================
308
+
309
+ def solve_lp_pulp_unified(c_coeffs, tipo_otimizacao, restricoes_parsed, integer_vars=False):
310
+ """
311
+ Resolve um problema de Programação Linear (PL) ou PL Inteira (PLI) usando PuLP.
312
+ Retorna a solução ótima (x1, x2), o valor da FO, preços sombra e custos reduzidos.
313
+ """
314
+ prob = pulp.LpProblem("Problema_PL", pulp.LpMaximize if tipo_otimizacao == 'maximizar' else pulp.LpMinimize)
315
+
316
+ # Define variáveis
317
+ if integer_vars:
318
+ x1 = pulp.LpVariable("x1", lowBound=0, cat='Integer')
319
+ x2 = pulp.LpVariable("x2", lowBound=0, cat='Integer')
320
+ else:
321
+ x1 = pulp.LpVariable("x1", lowBound=0, cat='Continuous')
322
+ x2 = pulp.LpVariable("x2", lowBound=0, cat='Continuous')
323
+
324
+ # Função Objetivo
325
+ prob += c_coeffs[0] * x1 + c_coeffs[1] * x2, "Funcao_Objetivo"
326
+
327
+ # Restrições
328
+ for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed):
329
+ if op_norm == 'le':
330
+ prob += a1 * x1 + a2 * x2 <= b, f"R{i+1}"
331
+ elif op_norm == 'ge':
332
+ prob += a1 * x1 + a2 * x2 >= b, f"R{i+1}"
333
+ elif op_norm == 'eq':
334
+ prob += a1 * x1 + a2 * x2 == b, f"R{i+1}"
335
+
336
+ # Resolver o problema
337
+ try:
338
+ prob.solve(pulp.PULP_CBC_CMD(msg=0)) # msg=0 para suprimir saída do solver
339
+ if prob.status == pulp.LpStatusOptimal:
340
+ optimal_point = (pulp.value(x1), pulp.value(x2))
341
+ optimal_z = pulp.value(prob.objective)
342
+
343
+ reduced_costs = {}
344
+ # Custos reduzidos são aplicáveis apenas para PL contínuo e se o solver forneceu
345
+ if not integer_vars:
346
+ # Verificar se o atributo existe antes de acessar
347
+ if hasattr(x1, 'reducedCost') and x1.reducedCost is not None:
348
+ reduced_costs['x1'] = x1.reducedCost
349
+ if hasattr(x2, 'reducedCost') and x2.reducedCost is not None:
350
+ reduced_costs['x2'] = x2.reducedCost
351
+
352
+ shadow_prices = {}
353
+ # Preços sombra são aplicáveis apenas para PL contínuo e se o solver forneceu
354
+ if not integer_vars:
355
+ for i, (a1, a2, op_norm, b) in enumerate(restricoes_parsed):
356
+ constraint_name = f"R{i+1}"
357
+ # Acessa a restrição pelo nome atribuído
358
+ c = prob.constraints[constraint_name]
359
+ # Verificar se o atributo existe antes de acessar
360
+ if hasattr(c, 'pi') and c.pi is not None:
361
+ shadow_prices[constraint_name] = c.pi
362
+
363
+ return optimal_point, optimal_z, reduced_costs, shadow_prices
364
+ else:
365
+ return None, None, None, None # Infactível, ilimitado ou outro status
366
+ except Exception as e:
367
+ print(f"Erro ao resolver LP com PuLP (integer_vars={integer_vars}): {e}")
368
+ return None, None, None, None
369
+
370
+ # ==============================================================================
371
+ # 3. FUNÇÃO WRAPPER PARA GRADIO (gradio_solver)
372
+ # ==============================================================================
373
+
374
+ def gradio_solver(question_reference, problem_description, x1_description, x2_description, c1_val, c2_val, opt_type, restrictions_df_input):
375
+
376
+ # 1. Parsear os coeficientes da função objetivo
377
+ try:
378
+ c_coeffs = [float(c1_val), float(c2_val)]
379
+ except ValueError:
380
+ # 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
381
+ 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()
382
+
383
+ # 2. Parsear as restrições do DataFrame
384
+ restricoes_parsed = []
385
+ if restrictions_df_input is not None and not restrictions_df_input.empty:
386
+ for row_idx, row_series in restrictions_df_input.iterrows(): # Iterar por linhas do DataFrame
387
+ row_list = row_series.values.tolist() # Converter a Series da linha para uma lista Python
388
+
389
+ # Ignorar linhas completamente vazias
390
+ if all(val is None or (isinstance(val, str) and val.strip() == '') for val in row_list):
391
+ continue
392
+
393
+ try:
394
+ a1 = float(row_list[0]) if row_list[0] is not None else 0.0
395
+ a2 = float(row_list[1]) if row_list[1] is not None else 0.0
396
+
397
+ op = str(row_list[2]).lower().strip() if row_list[2] is not None else ''
398
+ b = float(row_list[3]) if row_list[3] is not None else 0.0
399
+
400
+ # Normalização dos operadores para PuLP e is_factible
401
+ op_norm = ''
402
+ if op == '<=' or op == '<': op_norm = 'le'
403
+ elif op == '>=' or op == '>': op_norm = 'ge'
404
+ elif op == '=': op_norm = 'eq'
405
+ else: raise ValueError(f"Operador inválido '{op}' na restrição {row_idx+1}. Use '<', '<=', '=', '>=', ou '>'.")
406
+
407
+ restricoes_parsed.append((a1, a2, op_norm, b))
408
+ except Exception as e:
409
+ # 11 outputs
410
+ 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()
411
+
412
+ if not restricoes_parsed:
413
+ # 11 outputs
414
+ 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()
415
+
416
+ # --- Resolver o problema contínuo com PuLP para extrair preços sombra e custos reduzidos ---
417
+ continuous_pulp_point, continuous_pulp_z, reduced_costs, shadow_prices = \
418
+ solve_lp_pulp_unified(c_coeffs, opt_type, restricoes_parsed, integer_vars=False)
419
+
420
+ # --- Resolver o problema inteiro com PuLP ---
421
+ integer_solution_point, integer_optimal_z, _, _ = \
422
+ solve_lp_pulp_unified(c_coeffs, opt_type, restricoes_parsed, integer_vars=True)
423
+
424
+ # --- Gerar o gráfico FINAL (re-chamar resolver_graficamente com o ponto inteiro, se encontrado) ---
425
+ final_plot_result = resolver_graficamente(c_coeffs, opt_type, restricoes_parsed, integer_solution_point=integer_solution_point)
426
+
427
+ # --- Preparar as saídas para o Gradio ---
428
+ output_text = f"## Resolução do Problema de PL\n\n" \
429
+ f"**Função Objetivo:** {final_plot_result['funcao_objetivo']}\n" \
430
+ f"**Status da Região Fact��vel (Contínua):** {final_plot_result['regiao_factivel_status']}\n"
431
+
432
+ continuous_coords_output_str = "N/A"
433
+ continuous_z_output_str = "N/A"
434
+ integer_coords_output_str = "N/A"
435
+ integer_z_output_str = "N/A"
436
+ integer_sol_info = ""
437
+ shadow_prices_output_str = "N/A"
438
+ reduced_costs_output_str = "N/A"
439
+
440
+ if final_plot_result['regiao_factivel_status'] == 'OK':
441
+ output_text += f"\n**Vértices da Região Factível (Contínua):**\n"
442
+ for v_info in final_plot_result['vertices_info']:
443
+ output_text += f"- {v_info['nome']}: ({v_info['coordenadas'][0]:.2f}, {v_info['coordenadas'][1]:.2f}) -> Z = {v_info['valor_z']:.2f}\n"
444
+
445
+ output_text += f"\n**Solução Ótima Contínua:** {final_plot_result['solucao_tipo_msg']}\n"
446
+ if continuous_pulp_point is not None:
447
+ output_text += f"**Ponto(s) Ótimo(s) Contínuo:** ({continuous_pulp_point[0]:.2f}, {continuous_pulp_point[1]:.2f})\n" \
448
+ f"**Valor Ótimo Contínuo de Z:** {continuous_pulp_z:.2f}\n"
449
+ continuous_coords_output_str = f"({continuous_pulp_point[0]:.2f}, {continuous_pulp_point[1]:.2f})"
450
+ continuous_z_output_str = f"{continuous_pulp_z:.2f}"
451
+ else:
452
+ output_text += f"Não foi possível determinar a solução contínua pelo PuLP.\n"
453
+
454
+ # Formatar Preços Sombra e Custos Reduzidos
455
+ if shadow_prices:
456
+ shadow_prices_str = "\n".join([f" - {k}: {v:.4f}" for k, v in shadow_prices.items()])
457
+ output_text += f"\n**Preços Sombra das Restrições:**\n{shadow_prices_str}\n"
458
+ shadow_prices_output_str = shadow_prices_str
459
+ else:
460
+ output_text += f"\n**Preços Sombra das Restrições:** N/A (Não calculados ou problema infactível/ilimitado)\n"
461
+
462
+ if reduced_costs:
463
+ reduced_costs_str = "\n".join([f" - {k}: {v:.4f}" for k, v in reduced_costs.items()])
464
+ output_text += f"\n**Custos Reduzidos das Variáveis:**\n{reduced_costs_str}\n"
465
+ reduced_costs_output_str = reduced_costs_str
466
+ else:
467
+ output_text += f"\n**Custos Reduzidos das Variáveis:** N/A (Não calculados ou problema infactível/ilimitado)\n"
468
+
469
+
470
+ if integer_solution_point is not None:
471
+ integer_coords_output_str = f"({integer_solution_point[0]:.0f}, {integer_solution_point[1]:.0f})"
472
+ integer_z_output_str = f"{integer_optimal_z:.2f}"
473
+
474
+ solution_coincides = False
475
+ if continuous_pulp_point is not None and \
476
+ np.isclose(continuous_pulp_point[0], integer_solution_point[0], atol=1e-6) and \
477
+ np.isclose(continuous_pulp_point[1], integer_solution_point[1], atol=1e-6) and \
478
+ np.isclose(continuous_pulp_z, integer_optimal_z, atol=1e-2):
479
+ solution_coincides = True
480
+
481
+ if solution_coincides:
482
+ 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"
483
+ else:
484
+ integer_sol_info += f"**Ponto Ótimo Inteiro:** {integer_coords_output_str}\n" \
485
+ f"**Valor Ótimo Inteiro de Z:** {integer_z_output_str}\n"
486
+ output_text += f"\n**Solução Ótima Inteira:**\n" + integer_sol_info
487
+ else:
488
+ integer_sol_info = "Não foi encontrada uma solução inteira factível ou o problema é infactível para inteiros."
489
+ output_text += f"\n**Solução Ótima Inteira:** {integer_sol_info}\n"
490
+ else:
491
+ integer_sol_info = "Não aplicável, pois a região factível contínua não existe ou é ilimitada."
492
+ output_text += f"\n**Solução Ótima Inteira:** {integer_sol_info}\n"
493
+
494
+ # Preparar dados para o relatório PDF
495
+ report_data_for_pdf = {
496
+ 'question_reference': question_reference,
497
+ 'problem_description': problem_description,
498
+ 'x1_description': x1_description,
499
+ 'x2_description': x2_description,
500
+ 'output_markdown': output_text,
501
+ 'shadow_prices': shadow_prices,
502
+ 'reduced_costs': reduced_costs,
503
+ }
504
+
505
+ return output_text, final_plot_result['pil_image'], \
506
+ continuous_coords_output_str, \
507
+ continuous_z_output_str, \
508
+ integer_coords_output_str, \
509
+ integer_z_output_str, \
510
+ shadow_prices_output_str, \
511
+ reduced_costs_output_str, \
512
+ final_plot_result['regiao_factivel_status'], \
513
+ report_data_for_pdf, \
514
+ final_plot_result['bytes_io_for_pdf'] # Retorna bytes_io_for_pdf para o plot_buffer_state
515
+
516
+ # ==============================================================================
517
+ # 3.1 FUNÇÃO PARA GERAR RELATÓRIO PDF
518
+ # ==============================================================================
519
+
520
+ class PDF(FPDF):
521
+ def __init__(self, orientation='P', unit='mm', format='A4', question_reference_raw="Relatório de PL"):
522
+ super().__init__(orientation, unit, format)
523
+ self.question_reference = sanitize_for_fpdf_latin1(question_reference_raw) # Sanitizar aqui
524
+
525
+ def header(self):
526
+ self.set_font('Arial', 'B', 15)
527
+ self.cell(0, 10, self.question_reference, 0, 1, 'C') # Usa a referência como título
528
+ self.ln(10)
529
+
530
+ def footer(self):
531
+ self.set_y(-15)
532
+ self.set_font('Arial', 'I', 8)
533
+ self.cell(0, 10, f'Página {self.page_no()}/{{nb}}', 0, 0, 'C')
534
+
535
+ def generate_pdf(report_data, plot_figure_io_for_pdf):
536
+ tmp_png_path = None
537
+ tmp_pdf_path = None
538
+ try:
539
+ # Passa a referência da questão para o construtor do PDF
540
+ pdf = PDF(question_reference_raw=report_data.get('question_reference', 'Relatório de PL'))
541
+ pdf.alias_nb_pages()
542
+ pdf.add_page()
543
+ pdf.set_font('Arial', '', 12)
544
+
545
+ # Adicionar descrição do problema (NOVO)
546
+ if report_data.get('problem_description'):
547
+ pdf.set_font('Arial', 'B', 12)
548
+ pdf.multi_cell(0, 7, 'Descrição do Problema:')
549
+ pdf.set_font('Arial', '', 10)
550
+ pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['problem_description'])) # Sanitizar aqui
551
+ pdf.ln(5)
552
+
553
+ # Adicionar descrição de X1 (NOVO)
554
+ if report_data.get('x1_description'):
555
+ pdf.set_font('Arial', 'B', 12)
556
+ pdf.multi_cell(0, 7, 'Variável X1:')
557
+ pdf.set_font('Arial', '', 10)
558
+ pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['x1_description'])) # Sanitizar aqui
559
+ pdf.ln(5)
560
+
561
+ # Adicionar descrição de X2 (NOVO)
562
+ if report_data.get('x2_description'):
563
+ pdf.set_font('Arial', 'B', 12)
564
+ pdf.multi_cell(0, 7, 'Variável X2:')
565
+ pdf.set_font('Arial', '', 10)
566
+ pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(report_data['x2_description'])) # Sanitizar aqui
567
+ pdf.ln(5)
568
+
569
+ # Adicionar o conteúdo markdown principal da solução
570
+ pdf.set_font('Arial', 'B', 12)
571
+ pdf.multi_cell(0, 7, 'Análise da Solução:')
572
+ pdf.set_font('Arial', '', 10)
573
+ formatted_text = report_data['output_markdown'].replace('##', '').replace('**', '').replace('\n', '\n').strip()
574
+ pdf.multi_cell(0, 7, sanitize_for_fpdf_latin1(formatted_text)) # Sanitizar aqui
575
+
576
+ pdf.ln(5)
577
+ pdf.set_font('Arial', 'B', 12)
578
+ pdf.multi_cell(0, 7, 'Detalhes Adicionais:')
579
+ pdf.set_font('Arial', '', 10)
580
+
581
+ # Adicionar Preços Sombra
582
+ if 'shadow_prices' in report_data and report_data['shadow_prices']:
583
+ pdf.multi_cell(0, 7, 'Preços Sombra (Variáveis Duais):')
584
+ for k, v in report_data['shadow_prices'].items():
585
+ pdf.multi_cell(0, 5, sanitize_for_fpdf_latin1(f'- {k}: {v:.4f}')) # Sanitizar aqui
586
+ else:
587
+ pdf.multi_cell(0, 7, 'Preços Sombra: N/A')
588
+
589
+ # Adicionar Custos Reduzidos
590
+ if 'reduced_costs' in report_data and report_data['reduced_costs']:
591
+ pdf.multi_cell(0, 7, 'Custos Reduzidos (Variáveis Primal):')
592
+ for k, v in report_data['reduced_costs'].items():
593
+ pdf.multi_cell(0, 5, sanitize_for_fpdf_latin1(f'- {k}: {v:.4f}')) # Sanitizar aqui
594
+ else:
595
+ pdf.multi_cell(0, 7, 'Custos Reduzidos: N/A')
596
+
597
+ # Adicionar gráfico
598
+ if plot_figure_io_for_pdf and plot_figure_io_for_pdf.getbuffer().nbytes > 0:
599
+ pdf.add_page()
600
+ pdf.set_font('Arial', 'B', 12)
601
+ pdf.cell(0, 10, 'Gráfico da Solução:', 0, 1, 'L')
602
+
603
+ plot_figure_io_for_pdf.seek(0)
604
+
605
+ # Criar um arquivo temporário para a imagem PNG
606
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file:
607
+ tmp_file.write(plot_figure_io_for_pdf.getvalue())
608
+ tmp_png_path = tmp_file.name
609
+
610
+ pdf.image(tmp_png_path, x=10, y=pdf.get_y(), w=180)
611
+
612
+ # Gerar PDF bytes
613
+ pdf_string_output = pdf.output(dest='S')
614
+ pdf_bytes_output = pdf_string_output.encode('latin-1')
615
+
616
+ # Salvar PDF bytes para um arquivo temporário
617
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_pdf_file:
618
+ tmp_pdf_file.write(pdf_bytes_output)
619
+ tmp_pdf_path = tmp_pdf_file.name
620
+
621
+ # Retorna o PATH para o arquivo PDF temporário. Gradio irá gerenciar o download e a limpeza.
622
+ return gr.update(value=tmp_pdf_path, label="Download Relatório PDF", visible=True)
623
+ finally:
624
+ # Garante que o arquivo PNG temporário seja excluído, se foi criado
625
+ if tmp_png_path and os.path.exists(tmp_png_path):
626
+ os.remove(tmp_png_path)
627
+ # Não excluímos tmp_pdf_path aqui, pois Gradio precisa dele para download e deve limpá-lo.
628
+
629
+
630
+ # ==============================================================================
631
+ # 4. DEFINIÇÃO E LANÇAMENTO DA INTERFACE GRADIO (usando gr.Blocks)
632
+ # ==============================================================================
633
+
634
+ with gr.Blocks(title="Resolvedor LP Gráfico (2 Variáveis) com Solução Inteira") as demo:
635
+ gr.Markdown("# Universidade de Brasília – UnB")
636
+ gr.Markdown("### Programa de Pós-graduação em Computação Aplicada – PPCA")
637
+ gr.Markdown("## Mestrado Profissional")
638
+ gr.Markdown("### Fundamentos em Pesquisa Operacional – 2025/2")
639
+ gr.Markdown("#### Professor: Peng Yaohao")
640
+ gr.Markdown("#### Alunos: Douglas Lopes dos Santos, Éder Marcelo P. Cunha, Gilson Araújo e Pedro Britto Junior")
641
+ gr.Markdown("---")
642
+
643
+ 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.")
644
+ 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`.")
645
+ gr.Markdown("---")
646
+
647
+ with gr.Row():
648
+ with gr.Column(scale=1):
649
+ gr.Markdown("### 1. Detalhes do Problema")
650
+ 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")
651
+ 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="")
652
+ x1_description_input = gr.Textbox(label="O que é X1?", placeholder="Ex: Quantidade de produto A em unidades", value="")
653
+ x2_description_input = gr.Textbox(label="O que é X2?", placeholder="Ex: Quantidade de produto B em unidades", value="")
654
+
655
+ c1_input = gr.Number(label="Coeficiente c1 (para x1)", value=2.0)
656
+ c2_input = gr.Number(label="Coeficiente c2 (para x2)", value=3.0)
657
+ opt_type_radio = gr.Radio(["maximizar", "minimizar"], label="Tipo de Otimização", value="maximizar")
658
+
659
+ with gr.Column(scale=2):
660
+ gr.Markdown("### 2. Restrições")
661
+ default_restrictions = [[1, 1, "<=", 5], [2, 1, "<=", 8]]
662
+
663
+ restrictions_dataframe = gr.Dataframe(
664
+ headers=["x1 Coef", "x2 Coef", "Operador", "RHS"],
665
+ # OPERADORES RESTRITOS AGORA
666
+ datatype=["number", "number", {"choices": [">", ">=", "=", "<", "<="], "type": "str"}, "number"],
667
+ value=default_restrictions,
668
+ row_count=(len(default_restrictions), "dynamic"),
669
+ column_count=(4, "fixed"),
670
+ label="Defina as Restrições. Linhas vazias ou incompletas serão ignoradas.",
671
+ interactive=True
672
+ )
673
+
674
+ add_row_btn = gr.Button("Adicionar Linha de Restrição", size="sm")
675
+
676
+ def add_restriction_row_to_df(current_df_value: pd.DataFrame):
677
+ data_as_list = []
678
+ if current_df_value is not None and not current_df_value.empty:
679
+ data_as_list = current_df_value.values.tolist()
680
+
681
+ new_row = [None, None, "<=", None]
682
+ data_as_list.append(new_row)
683
+
684
+ return gr.update(value=data_as_list, row_count=(len(data_as_list), "dynamic"))
685
+
686
+ add_row_btn.click(
687
+ fn=add_restriction_row_to_df,
688
+ inputs=[restrictions_dataframe],
689
+ outputs=[restrictions_dataframe]
690
+ )
691
+
692
+ with gr.Row():
693
+ solve_btn = gr.Button("3. Resolver Problema", variant="primary", size="lg")
694
+ clear_btn = gr.Button("Limpar Campos", variant="secondary", size="lg")
695
+
696
+ gr.Markdown("---") # Separador visual
697
+
698
+ output_markdown = gr.Markdown(label="Detalhes da Resolução")
699
+ 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)
700
+
701
+ with gr.Row():
702
+ continuous_coords_output = gr.Textbox(label="Ponto(s) Ótimo(s) Contínuo", interactive=False)
703
+ continuous_z_output = gr.Textbox(label="Valor Ótimo Contínuo de Z", interactive=False)
704
+ with gr.Row():
705
+ integer_coords_output = gr.Textbox(label="Ponto Ótimo Inteiro", interactive=False)
706
+ integer_z_output = gr.Textbox(label="Valor Ótimo Inteiro de Z", interactive=False)
707
+ with gr.Row():
708
+ shadow_prices_output = gr.Textbox(label="Preços Sombra (Restrições)", lines=3, interactive=False)
709
+ reduced_costs_output = gr.Textbox(label="Custos Reduzidos (Variáveis)", lines=3, interactive=False)
710
+
711
+ status_output = gr.Textbox(label="Status da Região Factível (Contínua)", interactive=False)
712
+
713
+ report_data_state = gr.State(value={}) # Para armazenar dados para o PDF
714
+ plot_buffer_state = gr.State(value=io.BytesIO())
715
+
716
+ pdf_download_btn = gr.Button("Gerar Relatório PDF", variant="secondary")
717
+ # O gr.File agora será do tipo "filepath" e receberá um caminho de arquivo
718
+ pdf_output_file = gr.File(label="Relatório PDF", file_count="single", interactive=False, visible=False, type="filepath")
719
+
720
+
721
+ # Define o manipulador de eventos para o botão Resolver
722
+ solve_btn.click(
723
+ fn=gradio_solver,
724
+ inputs=[
725
+ question_reference_input, problem_description_input, x1_description_input, x2_description_input, # Novos inputs
726
+ c1_input, c2_input, opt_type_radio, restrictions_dataframe
727
+ ],
728
+ outputs=[
729
+ output_markdown, output_plot,
730
+ continuous_coords_output, continuous_z_output,
731
+ integer_coords_output, integer_z_output,
732
+ shadow_prices_output, reduced_costs_output,
733
+ status_output, report_data_state, plot_buffer_state
734
+ ]
735
+ )
736
+
737
+ # Manipulador de eventos para o botão de PDF
738
+ pdf_download_btn.click(
739
+ fn=generate_pdf,
740
+ inputs=[report_data_state, plot_buffer_state], # Pega os dados do estado e o buffer do plot
741
+ outputs=[pdf_output_file]
742
+ )
743
+
744
+ # Função para limpar todos os campos
745
+ def clear_all_inputs():
746
+ return (
747
+ gr.update(value="Trabalho Final FPO"), # question_reference_input
748
+ gr.update(value=""), # problem_description_input
749
+ gr.update(value=""), # x1_description_input
750
+ gr.update(value=""), # x2_description_input
751
+ gr.update(value=2.0), # c1_input
752
+ gr.update(value=3.0), # c2_input
753
+ gr.update(value="maximizar"), # opt_type_radio
754
+ gr.update(value=[[1, 1, "<=", 5], [2, 1, "<=", 8]]), # restrictions_dataframe
755
+ gr.update(value=""), # output_markdown
756
+ gr.update(value=None), # output_plot (limpa a imagem)
757
+ gr.update(value="N/A"), # continuous_coords_output
758
+ gr.update(value="N/A"), # continuous_z_output
759
+ gr.update(value="N/A"), # integer_coords_output
760
+ gr.update(value="N/A"), # integer_z_output
761
+ gr.update(value="N/A"), # shadow_prices_output
762
+ gr.update(value="N/A"), # reduced_costs_output
763
+ gr.update(value=""), # status_output
764
+ {}, # report_data_state (reset state)
765
+ io.BytesIO(), # plot_buffer_state (reset state)
766
+ gr.File(label="Relatório PDF", file_count="single", interactive=False, visible=False, type="filepath") # pdf_output_file
767
+ )
768
+
769
+ clear_btn.click(
770
+ fn=clear_all_inputs,
771
+ outputs=[
772
+ question_reference_input, problem_description_input, x1_description_input, x2_description_input,
773
+ c1_input, c2_input, opt_type_radio, restrictions_dataframe,
774
+ output_markdown, output_plot,
775
+ continuous_coords_output, continuous_z_output,
776
+ integer_coords_output, integer_z_output,
777
+ shadow_prices_output, reduced_costs_output,
778
+ status_output, report_data_state, plot_buffer_state,
779
+ pdf_output_file
780
+ ]
781
+ )
782
+
783
+
784
+ if __name__ == "__main__":
785
+ demo.launch()