Spaces:
Sleeping
Sleeping
| import re | |
| import traceback | |
| from typing import List, Dict, Any, Tuple | |
| import numpy as np | |
| import pandas as pd | |
| import gradio as gr | |
| from fpdf import FPDF | |
| EPS = 1e-9 | |
| # ---------------- Parsing utilities ---------------- | |
| def parse_coeffs(text: str) -> List[float]: | |
| if not text or not text.strip(): | |
| return [] | |
| s = text.replace(',', ' ') | |
| parts = [p for p in s.split() if p.strip()] | |
| coeffs = [] | |
| for p in parts: | |
| try: | |
| coeffs.append(float(eval(p))) | |
| except Exception: | |
| raise ValueError(f"Coeficiente inválido: '{p}'") | |
| return coeffs | |
| def parse_constraints(text: str, nvars: int) -> Tuple[List[Dict[str,Any]], List[int]]: | |
| lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()] | |
| skip_words = ["tal que", "sujeito a", "subject to", "s.t.", "st:"] | |
| lines = [ln for ln in lines if not any(word in ln.lower() for word in skip_words)] | |
| free_vars = [] | |
| cons = [] | |
| pattern_free = re.compile(r'x([0-9]+)\s*(livre|free)', flags=re.I) | |
| term_pattern = r'([+-]?[0-9./]*)(x[0-9]+)' | |
| for ln in lines[:]: | |
| m = pattern_free.search(ln) | |
| if m: | |
| idx = int(m.group(1)) - 1 | |
| if idx < 0 or idx >= nvars: | |
| raise ValueError(f"Variável livre fora do intervalo: x{idx+1}") | |
| free_vars.append(idx) | |
| lines.remove(ln) | |
| for ln in lines: | |
| s = ln.replace(" ", "") | |
| if "<=" in s or "=<" in s: | |
| s = s.replace("=<", "<=") | |
| left, right = s.split("<=") | |
| sense = "<=" | |
| elif ">=" in s or "=>" in s: | |
| s = s.replace("=>", ">=") | |
| left, right = s.split(">=") | |
| sense = ">=" | |
| elif "=" in s: | |
| left, right = s.split("=") | |
| sense = "=" | |
| else: | |
| raise ValueError(f"Faltando <=, >= ou =: '{ln}'") | |
| try: | |
| rhs = float(eval(right)) | |
| except Exception: | |
| raise ValueError(f"RHS inválido em: '{ln}'") | |
| coeffs = [0.0] * nvars | |
| terms = re.findall(term_pattern, left) | |
| for coef_str, var_str in terms: | |
| idx = int(var_str[1:]) - 1 | |
| if coef_str in ["", "+"]: | |
| v = 1.0 | |
| elif coef_str == "-": | |
| v = -1.0 | |
| else: | |
| v = float(eval(coef_str)) | |
| coeffs[idx] += v | |
| cons.append({'coeffs': coeffs, 'sense': sense, 'rhs': rhs}) | |
| return cons, sorted(list(set(free_vars))) | |
| def expand_free_variables(nvars: int, c: List[float], constraints: List[Dict[str,Any]], free_vars: List[int]): | |
| new_c = [] | |
| mapping = {} | |
| for i in range(nvars): | |
| if i in free_vars: | |
| new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1) | |
| new_c.append(-c[i]); mapping[len(new_c)-1] = (i, -1) | |
| else: | |
| new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1) | |
| new_constraints = [] | |
| for row in constraints: | |
| coeffs = row['coeffs'] | |
| new_coeffs = [] | |
| for i in range(nvars): | |
| if i in free_vars: | |
| new_coeffs.append(coeffs[i]) | |
| new_coeffs.append(-coeffs[i]) | |
| else: | |
| new_coeffs.append(coeffs[i]) | |
| new_constraints.append({'coeffs': new_coeffs, 'sense': row['sense'], 'rhs': row['rhs']}) | |
| return len(new_c), new_c, new_constraints, mapping | |
| # ---------------- Tableau helpers ---------------- | |
| def snapshot_html(tableau: np.ndarray, basis: List[int]) -> str: | |
| cols = tableau.shape[1] | |
| html = '<table border="1" style="border-collapse:collapse;font-family:Arial; font-size:12px;">' | |
| for i in range(tableau.shape[0]): | |
| html += '<tr>' | |
| for j in range(cols): | |
| val = tableau[i, j] | |
| html += f'<td style="padding:4px;">{val:.6g}</td>' | |
| html += '</tr>' | |
| html += '</table>' | |
| return html | |
| def primal_simplex_tableau(T: np.ndarray, basis: List[int], max_iters=1000) -> Tuple[np.ndarray, List[int], List[Dict[str,Any]]]: | |
| m = T.shape[0] - 1 | |
| ncols = T.shape[1] | |
| path = [] | |
| path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)}) | |
| it = 0 | |
| while it < max_iters: | |
| it += 1 | |
| obj_row = T[-1, :-1] | |
| entering_candidates = np.where(obj_row < -EPS)[0] | |
| if entering_candidates.size == 0: | |
| break | |
| entering = int(entering_candidates[0]) | |
| ratios = np.full(m, np.inf) | |
| for i in range(m): | |
| a = T[i, entering] | |
| if a > EPS: | |
| ratios[i] = T[i, -1] / a | |
| if np.all(np.isinf(ratios)): | |
| raise Exception('Unbounded LP') | |
| leaving = int(np.argmin(ratios)) | |
| piv = T[leaving, entering] | |
| T[leaving, :] = T[leaving, :] / piv | |
| for i in range(m+1): | |
| if i == leaving: continue | |
| T[i, :] = T[i, :] - T[i, entering] * T[leaving, :] | |
| basis[leaving] = entering | |
| path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)}) | |
| return T, basis, path | |
| # ---------------- Two-Phase implementation (CORRIGIDA) ---------------- | |
| def build_tableau_two_phase(c: List[float], constraints: List[Dict[str,Any]], sense: str = 'max'): | |
| obj_mult = 1.0 | |
| if sense == 'min': | |
| obj_mult = -1.0 | |
| c_adj = [ci * obj_mult for ci in c] | |
| n = len(c_adj) | |
| m = len(constraints) | |
| slacks = 0 | |
| artificials = 0 | |
| for row in constraints: | |
| if row['sense'] == '<=': | |
| slacks += 1 | |
| elif row['sense'] == '>=': | |
| slacks += 1 | |
| artificials += 1 | |
| else: | |
| artificials += 1 | |
| total_cols = n + slacks + artificials + 1 | |
| T = np.zeros((m + 1, total_cols)) | |
| slack_idx = n | |
| artificial_idx = n + slacks | |
| basis = [] | |
| art_positions = [] | |
| s_counter = 0 | |
| a_counter = 0 | |
| for i, row in enumerate(constraints): | |
| coeffs = row['coeffs'] | |
| T[i, :n] = coeffs | |
| if row['sense'] == '<=': | |
| T[i, slack_idx + s_counter] = 1.0 | |
| basis.append(slack_idx + s_counter) | |
| s_counter += 1 | |
| elif row['sense'] == '>=': | |
| T[i, slack_idx + s_counter] = -1.0 | |
| T[i, artificial_idx + a_counter] = 1.0 | |
| basis.append(artificial_idx + a_counter) | |
| art_positions.append(artificial_idx + a_counter) | |
| s_counter += 1 | |
| a_counter += 1 | |
| else: # equality | |
| T[i, artificial_idx + a_counter] = 1.0 | |
| basis.append(artificial_idx + a_counter) | |
| art_positions.append(artificial_idx + a_counter) | |
| a_counter += 1 | |
| T[i, -1] = row['rhs'] | |
| # Phase I objective: minimize sum of artificials. | |
| # Convert to maximization for our tableau solver: maximize (-sum a_j) | |
| # So c_phase1 (for maximization) = -1 for each artificial column. | |
| c_phase1 = np.zeros(total_cols - 1) | |
| for a in art_positions: | |
| c_phase1[a] = -1.0 | |
| # In tableau we store -c in last row, so set T[-1, :-1] = -c_phase1 | |
| T[-1, :-1] = -c_phase1 | |
| # But because artificials are in basis, we must adjust objective row: | |
| # T[-1, :] = -c + sum_{i in basis} c_Bi * row_i, where c_Bi = c_phase1[basis_i] | |
| for i in range(m): | |
| bi = basis[i] | |
| cBi = c_phase1[bi] if bi < len(c_phase1) else 0.0 | |
| if abs(cBi) > EPS: | |
| T[-1, :] += cBi * T[i, :] | |
| return T, basis, (n, slacks, artificials), art_positions, c_adj | |
| def run_two_phase(c, constraints, sense='max'): | |
| #Implementação 100% por tableau — compatível com HuggingFace | |
| #Phase I + Phase II completas (sem SciPy). | |
| # ---------- PHASE I ---------- | |
| T0, basis0, (n_orig, n_slack, n_art), art_positions, c_adj = build_tableau_two_phase( | |
| c, constraints, sense | |
| ) | |
| try: | |
| T1, basis1, path1 = primal_simplex_tableau(T0.copy(), basis0.copy()) | |
| except Exception as e: | |
| return { | |
| 'status': 'phase1_failed', | |
| 'error': str(e), | |
| 'trace': traceback.format_exc() | |
| } | |
| phase1_obj = float(T1[-1, -1]) | |
| # se sum(a_j) != 0 → inviável | |
| if abs(phase1_obj) > 1e-6: | |
| return { | |
| 'status': 'infeasible', | |
| 'phase1_obj': phase1_obj, | |
| 'phase1_path': path1, | |
| 'tableau_phase1': T1 | |
| } | |
| # ---------- REMOVER ARTIFICIAIS ---------- | |
| art_cols = set(art_positions) | |
| old_ncols = T1.shape[1] - 1 | |
| keep_cols = [j for j in range(old_ncols) if j not in art_cols] | |
| # construir tableau da Phase II (T2) | |
| T2 = np.zeros((T1.shape[0], len(keep_cols) + 1)) | |
| for i, col in enumerate(keep_cols): | |
| T2[:, i] = T1[:, col] | |
| T2[:, -1] = T1[:, -1] | |
| # nova base | |
| basis2 = [] | |
| for bi in basis1: | |
| if bi in art_cols: | |
| basis2.append(None) | |
| else: | |
| basis2.append(keep_cols.index(bi)) | |
| # corrigir linhas onde a base ficou None | |
| used = set([b for b in basis2 if b is not None]) | |
| m = T2.shape[0] - 1 | |
| for i in range(m): | |
| if basis2[i] is None: | |
| replaced = False | |
| for j in range(T2.shape[1] - 1): | |
| if j not in used and abs(T2[i, j]) > EPS: | |
| piv = T2[i, j] | |
| T2[i, :] = T2[i, :] / piv | |
| for r in range(m+1): | |
| if r != i: | |
| T2[r, :] -= T2[r, j] * T2[i, :] | |
| basis2[i] = j | |
| used.add(j) | |
| replaced = True | |
| break | |
| if not replaced: | |
| basis2[i] = None | |
| # ---------- PHASE II — definir objetivo original ---------- | |
| c_full = [] | |
| for col in keep_cols: | |
| if col < len(c_adj): | |
| c_full.append(c_adj[col]) | |
| else: | |
| c_full.append(0.0) | |
| c_full = np.array(c_full) | |
| T2[-1, :-1] = -c_full | |
| for i in range(m): | |
| bi = basis2[i] | |
| if bi is not None and bi < len(c_full): | |
| coef = c_full[bi] | |
| if abs(coef) > EPS: | |
| T2[-1, :] += coef * T2[i, :] | |
| # preencher bases ausentes | |
| for i in range(m): | |
| if basis2[i] is None: | |
| for j in range(T2.shape[1]-1): | |
| if j not in used: | |
| basis2[i] = j | |
| used.add(j) | |
| break | |
| # ---------- SIMPLEX PHASE II ---------- | |
| try: | |
| T_final, basis_final, path2 = primal_simplex_tableau(T2.copy(), basis2.copy()) | |
| except Exception as e: | |
| return { | |
| 'status': 'phase2_failed', | |
| 'error': str(e), | |
| 'phase1_path': path1, | |
| 'trace': traceback.format_exc() | |
| } | |
| # ---------- EXTRAI X*, REDUCED COSTS E SHADOW PRICES ---------- | |
| x = [0.0] * n_orig | |
| for i, bi in enumerate(basis_final): | |
| if bi is not None: | |
| oldcol = keep_cols[bi] | |
| if oldcol < n_orig: | |
| x[oldcol] = float(T_final[i, -1]) | |
| z = float(T_final[-1, -1]) | |
| # custos reduzidos apenas variáveis originais | |
| reduced = [] | |
| for j in range(n_orig): | |
| if j in keep_cols: | |
| colpos = keep_cols.index(j) | |
| z_j = -T_final[-1, colpos] | |
| reduced.append(round(c_adj[j] - z_j, 8)) | |
| else: | |
| reduced.append(0.0) | |
| # preços-sombra = coeficientes de slack na linha da função objetivo | |
| shadow = [] | |
| for i in range(len(constraints)): | |
| col = n_orig + i | |
| if col in keep_cols: | |
| idx = keep_cols.index(col) | |
| shadow.append(round(-T_final[-1, idx], 8)) | |
| else: | |
| shadow.append(0.0) | |
| return { | |
| 'status': 'optimal', | |
| 'x': [round(v, 8) for v in x], | |
| 'obj': round(z, 8), | |
| 'path_phase1': path1, | |
| 'path_phase2': path2, | |
| 'tableau_final': T_final, | |
| 'basis_final': basis_final, | |
| 'reduced_costs': reduced, | |
| 'shadow_prices': shadow | |
| } | |
| # ---------------- Helpers & PDF ---------------- | |
| def clean_vector(vec): | |
| try: | |
| return [float(v) for v in vec] | |
| except: | |
| return vec | |
| def gerar_pdf_from_text(nome_arquivo: str, conteudo: str) -> str: | |
| caminho = "/mnt/data/" + nome_arquivo | |
| pdf = FPDF() | |
| pdf.set_auto_page_break(auto=True, margin=12) | |
| pdf.add_page() | |
| pdf.set_font("Arial", size=10) | |
| for line in str(conteudo).split("\n"): | |
| if line.strip() == "": | |
| pdf.ln(2) | |
| else: | |
| pdf.multi_cell(0, 5, line) | |
| pdf.output(caminho) | |
| return caminho | |
| def gerar_pdf_primal_dual_from_ui(model_txt, solution_html, steps_primal_html, steps_phase1_html, summary_txt): | |
| try: | |
| clean = lambda s: re.sub(r"<[^>]*>", "", str(s) if s is not None else "") | |
| model = clean(model_txt); solution = clean(solution_html) | |
| steps_p = clean(steps_primal_html); steps_1 = clean(steps_phase1_html) | |
| summary = clean(summary_txt) | |
| partes = ["MODELO",model,"","SOLUÇÃO (tabela)",solution,"","PASSOS (Phase II)",steps_p,"","PASSOS (Phase I)",steps_1,"","RESUMO",summary] | |
| texto = "\n".join(partes) | |
| caminho = gerar_pdf_from_text("resultado_simplex.pdf", texto) | |
| return caminho | |
| except Exception as e: | |
| err = "Erro ao gerar PDF:\n" + str(e) + "\n\n" + traceback.format_exc() | |
| caminho = gerar_pdf_from_text("erro_gerar_pdf.txt", err) | |
| return caminho | |
| # ---------------- Gradio handler ---------------- | |
| def run_algorithms(nvars_str, objective_str, cons_str, sense, mode): | |
| try: | |
| nvars = int(nvars_str) | |
| if nvars <= 0: | |
| return 'Erro: nvars deve ser inteiro positivo', '', '', '', '' | |
| c = parse_coeffs(objective_str) | |
| if len(c) != nvars: | |
| return 'Erro: coeficientes do objetivo não correspondem a nvars', '', '', '', '' | |
| constraints, free_vars = parse_constraints(cons_str, nvars) | |
| if free_vars: | |
| nvars, c, constraints, mapping = expand_free_variables(nvars, c, constraints, free_vars) | |
| except Exception as e: | |
| return f'Erro ao ler entrada: {e}', '', '', '', '' | |
| res = run_two_phase(c, constraints, sense) | |
| status = res.get('status') | |
| # infeasible detected in Phase I | |
| if status == 'infeasible': | |
| return f"Problema inviável (Phase I obj = {res.get('phase1_obj')})", '', '', '', '' | |
| # Phase I failed | |
| if status == 'phase1_failed': | |
| return f"Erro na Phase I: {res.get('error','(sem detalhe)')}", '', '', '', '' | |
| # Phase II via tableau succeeded (our run_two_phase returns 'optimal' in that case) | |
| if status == 'optimal': | |
| x_primal = res['x'] | |
| z_primal = res['obj'] | |
| reduced = res.get('reduced_costs', []) | |
| shadow = res.get('shadow_prices', []) | |
| T_final = res.get('tableau_final', None) | |
| path_primal = res.get('path_phase2', []) | |
| path_phase1 = res.get('path_phase1', []) | |
| # Phase II solved via scipy.linprog successfully | |
| elif status == 'optimal_linprog': | |
| x_primal = res.get('x', []) | |
| z_primal = res.get('obj', None) | |
| # reduced costs / shadow prices may not be available from linprog | |
| reduced = res.get('reduced_costs', []) | |
| shadow = res.get('shadow_prices', []) | |
| T_final = None | |
| path_primal = res.get('phase1_path', []) # we still have Phase I path | |
| path_phase1 = res.get('phase1_path', []) | |
| elif status in ('linprog_failed', 'no_scipy_or_error'): | |
| msg = res.get('linprog_message') or res.get('error') or 'linprog falhou (detalhes indisponíveis)' | |
| phase1_path = res.get('phase1_path', []) | |
| return f"Erro na resolução: {status} - {msg}", '', '', '', '' | |
| else: | |
| # fallback catch-all | |
| return f"Erro na resolução: status inesperado '{status}' - {res.get('error','')}", '', '', '', '' | |
| x_primal = res['x']; z_primal = res['obj'] | |
| reduced = res['reduced_costs']; shadow = res['shadow_prices'] | |
| T_final = res['tableau_final'] | |
| path2 = res['path_phase2']; path1 = res['path_phase1'] | |
| steps_html_phase2 = "" | |
| for idx, step in enumerate(path2): | |
| # step is dict with 'tableau' and 'basis' | |
| steps_html_phase2 += f"<h4>Phase II — Passo {idx+1} — Base: {step.get('basis','?')}</h4>" | |
| steps_html_phase2 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>" | |
| steps_html_phase1 = "" | |
| for idx, step in enumerate(path1): | |
| steps_html_phase1 += f"<h4>Phase I — Passo {idx+1} — Base: {step.get('basis','?')}</h4>" | |
| steps_html_phase1 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>" | |
| df = pd.DataFrame({'Variável': [f'x{i+1}' for i in range(len(x_primal))], 'Valor': x_primal}) | |
| solution_html = df.to_html(index=False) | |
| solution_html += f"<p><b>Valor ótimo (estimado) = {z_primal:.6g}</b></p>" | |
| x_primal = clean_vector(x_primal); reduced = clean_vector(reduced); shadow = clean_vector(shadow) | |
| z_primal = float(z_primal) | |
| model_txt = f"Objective ({'min' if sense=='min' else 'max'}): {c}\nConstraints:\n" | |
| for r in constraints: | |
| model_txt += f" {r['coeffs']} {r['sense']} {r['rhs']}\n" | |
| summary = "" | |
| summary += f"Solução primal x* = {x_primal}\n" | |
| summary += f"Z_primal (estimado) = {z_primal:.6g}\n" | |
| summary += f"Preços-sombra (dual estimado) = {shadow}\n" | |
| summary += f"Custos reduzidos (orig vars) = {reduced}\n" | |
| return model_txt, solution_html, steps_html_phase2, steps_html_phase1, summary | |
| # ---------------- Gradio UI ---------------- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Simplex — Duas Fases (Phase I / Phase II) — Educational") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| nvars = gr.Textbox(label='Número de variáveis (n)', value='2') | |
| objective = gr.Textbox(label='Coeficientes da função objetivo (ex: \"60 30\")', value='60 30') | |
| cons = gr.Textbox(label='Restrições (uma por linha). Ex.: 2x1 + 3x2 <= 300', lines=6, | |
| value='2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50') | |
| sense = gr.Radio(['max','min'], value='max', label='Tipo de objetivo') | |
| run = gr.Button('Executar Simplex (Duas Fases)') | |
| with gr.Column(scale=2): | |
| model_out = gr.Textbox(label='Função objetivo e restrições (modelo)', lines=6) | |
| solution_out = gr.HTML(label='Solução ótima (tabela)') | |
| steps_phase2_out = gr.HTML(label='Passos do Simplex (Phase II tableaus)') | |
| steps_phase1_out = gr.HTML(label='Passos do Simplex (Phase I tableaus)') | |
| summary_out = gr.Textbox(label='Resumo', lines=8) | |
| run.click(run_algorithms, inputs=[nvars, objective, cons, sense, gr.State(value='primal_and_dual')], outputs=[model_out, solution_out, steps_phase2_out, steps_phase1_out, summary_out]) | |
| btn_pdf = gr.Button("Gerar PDF") | |
| pdf_out = gr.File(label="Baixar PDF") | |
| btn_pdf.click( | |
| gerar_pdf_primal_dual_from_ui, | |
| inputs=[model_out, solution_out, steps_phase2_out, steps_phase1_out, summary_out], | |
| outputs=[pdf_out] | |
| ) | |
| gr.Examples(examples=[["2","60 30","2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50","max"]], inputs=[nvars, objective, cons, sense]) | |
| if __name__ == '__main__': | |
| demo.launch(ssr_mode=False) | |
| 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 N variáveis de decisão, calcula a solução ótima inteira e as exibe no na tela.") | |
| 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() | |
| """ | |
| import gradio as gr | |
| from typing import List, Tuple, Dict, Any | |
| import math | |
| import re | |
| EPS = 1e-9 | |
| # --------------------------- Linear Algebra Helpers --------------------------- | |
| def zeros(rows, cols): | |
| return [[0.0] * cols for _ in range(rows)] | |
| # --------------------------- Parser Utilities --------------------------- | |
| def parse_coeffs(text: str) -> List[float]: | |
| # Parseia coeficientes da função objetivo. | |
| if not text or not text.strip(): | |
| return [] | |
| s = text.replace(',', ' ') | |
| parts = [p for p in s.split() if p.strip()] | |
| coeffs = [] | |
| for p in parts: | |
| try: | |
| coeffs.append(float(eval(p))) | |
| except Exception: | |
| raise ValueError(f"Coeficiente inválido: '{p}'") | |
| return coeffs | |
| def parse_constraints(text: str, nvars: int) -> List[Dict[str,Any]]: | |
| lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()] | |
| cons = [] | |
| for ln in lines: | |
| s = ln.replace(" ", "") | |
| # operador lógico | |
| if "<=" in s or "=<" in s: | |
| s = s.replace("=<", "<=") | |
| left, right = s.split("<=") | |
| sense = "<=" | |
| elif ">=" in s or "=>" in s: | |
| s = s.replace("=>", ">=") | |
| left, right = s.split(">=") | |
| sense = ">=" | |
| elif "=" in s: | |
| left, right = s.split("=") | |
| sense = "=" | |
| else: | |
| raise ValueError(f"Restrição inválida, faltando <=, >= ou = : '{ln}'") | |
| # RHS | |
| try: | |
| rhs = float(eval(right)) | |
| except: | |
| raise ValueError(f"RHS inválido: '{right}'") | |
| # capturar termos tipo +2x1, -3x2, 6/5x1 | |
| pattern = r'([+-]?[0-9./]*)(x[0-9]+)' | |
| terms = re.findall(pattern, left) | |
| coeffs = [0.0] * nvars | |
| for coef_str, var_str in terms: | |
| idx = int(var_str[1:]) - 1 | |
| if idx < 0 or idx >= nvars: | |
| raise ValueError(f"Variável fora do intervalo: {var_str}") | |
| if coef_str in ["", "+"]: | |
| v = 1.0 | |
| elif coef_str == "-": | |
| v = -1.0 | |
| else: | |
| v = float(eval(coef_str)) | |
| coeffs[idx] += v | |
| cons.append({ | |
| "coeffs": coeffs, | |
| "sense": sense, | |
| "rhs": rhs | |
| }) | |
| return cons | |
| # --------------------------- Simplex Implementation --------------------------- | |
| def build_canonical_tableau(objective: List[float], constraints: List[Dict[str,Any]], sense: str = 'max'): | |
| c = objective[:] | |
| obj_mult = 1.0 | |
| if sense == 'min': | |
| obj_mult = -1.0 | |
| c_adj = [ci * obj_mult for ci in c] | |
| norm = [] | |
| for row in constraints: | |
| coeffs = row['coeffs'][:] | |
| rhs = row['rhs'] | |
| s = row['sense'] | |
| if s == '>=': | |
| coeffs = [-a for a in coeffs] | |
| rhs = -rhs | |
| s = '<=' | |
| norm.append({"coeffs": coeffs, "rhs": rhs, "sense": s}) | |
| m = len(norm) | |
| n = len(c_adj) | |
| T = zeros(m + 1, n + m + 1) | |
| for i in range(m): | |
| for j in range(n): | |
| T[i][j] = norm[i]['coeffs'][j] | |
| T[i][n + i] = 1.0 | |
| T[i][n + m] = norm[i]['rhs'] | |
| for j in range(n): | |
| T[m][j] = -c_adj[j] | |
| return T, n, m, c, obj_mult, norm | |
| def extract_solution_from_tableau(T, basis, n, m, original_c): | |
| x = [0.0] * n | |
| for i in range(m): | |
| if basis[i] < n: | |
| x[basis[i]] = T[i][n + m] | |
| obj = sum(original_c[j] * x[j] for j in range(n)) | |
| return x, obj | |
| def snapshot(T, basis, n, m): | |
| x = [0.0] * n | |
| for i in range(m): | |
| if basis[i] < n: | |
| x[basis[i]] = T[i][n + m] | |
| return { | |
| "basis": basis[:], | |
| "x": [float(f"{v:.6f}") for v in x], | |
| "tableau": [row[:] for row in T] | |
| } | |
| def pivot(T, row, col): | |
| piv = T[row][col] | |
| if abs(piv) < EPS: | |
| raise ZeroDivisionError("Pivot quase zero") | |
| cols = len(T[0]) | |
| rows = len(T) | |
| for j in range(cols): | |
| T[row][j] /= piv | |
| for i in range(rows): | |
| if i == row: | |
| continue | |
| factor = T[i][col] | |
| for j in range(cols): | |
| T[i][j] -= factor * T[row][j] | |
| def primal_simplex(objective, constraints, sense): | |
| T, n, m, original_c, obj_mult, norm = build_canonical_tableau(objective, constraints, sense) | |
| basis = [n + i for i in range(m)] | |
| path = [snapshot(T, basis, n, m)] | |
| for _ in range(500): | |
| entering = next((j for j in range(n + m) if T[m][j] < -EPS), None) | |
| if entering is None: | |
| break | |
| best = math.inf | |
| leaving = None | |
| for i in range(m): | |
| a = T[i][entering] | |
| if a > EPS: | |
| r = T[i][n + m] / a | |
| if r < best: | |
| best = r | |
| leaving = i | |
| if leaving is None: | |
| return {"status": "unbounded", "path": path} | |
| pivot(T, leaving, entering) | |
| basis[leaving] = entering | |
| path.append(snapshot(T, basis, n, m)) | |
| x, obj = extract_solution_from_tableau(T, basis, n, m, original_c) | |
| reduced = [float(f"{(original_c[j] + T[m][j]):.6f}") for j in range(n)] | |
| shadow = [float(f"{(-T[m][n + i]):.6f}") for i in range(m)] | |
| return { | |
| "status": "optimal", | |
| "x": x, | |
| "obj": obj, | |
| "path": path, | |
| "reduced_costs": reduced, | |
| "shadow_prices": shadow | |
| } | |
| def dual_simplex(objective, constraints, sense): | |
| # usa mesma implementação — ok | |
| return primal_simplex(objective, constraints, sense) | |
| # --------------------------- Gradio App UI --------------------------- | |
| def run_algorithms(nvars_str, objective_str, cons_str, sense): | |
| try: | |
| nvars = int(nvars_str) | |
| objective = parse_coeffs(objective_str) | |
| constraints = parse_constraints(cons_str, nvars) | |
| if len(objective) != nvars: | |
| return "Erro: objetivo não bate com nvars", "", "" | |
| except Exception as e: | |
| return f"Erro ao ler entrada: {e}", "", "" | |
| primal = primal_simplex(objective, constraints, sense) | |
| dual = dual_simplex(objective, constraints, sense) | |
| def format_res(res): | |
| if res["status"] != "optimal": | |
| return f"Status: {res['status']}" | |
| out = [] | |
| out.append(f"Solução x* = {res['x']}") | |
| out.append(f"Valor objetivo = {res['obj']}") | |
| out.append(f"Preços-sombra = {res['shadow_prices']}") | |
| out.append(f"Custos reduzidos = {res['reduced_costs']}") | |
| out.append(f"\n--- Path ({len(res['path'])}) ---") | |
| for k, step in enumerate(res["path"]): | |
| out.append(f"\n### Passo {k+1}") | |
| out.append(f"Base = {step['basis']}") | |
| out.append(f"x = {step['x']}") | |
| out.append("Tableau:") | |
| for row in step["tableau"]: | |
| out.append(" " + " ".join(f"{v:8.3f}" for v in row)) | |
| return "\n".join(out) | |
| model_txt = "Modelo:\nObjetivo: " + str(objective) + "\nRestrições:\n" | |
| for c in constraints: | |
| model_txt += f"{c['coeffs']} {c['sense']} {c['rhs']}\n" | |
| return model_txt, format_res(primal), format_res(dual) | |
| def demo_input(): | |
| return "2", "60,30", "2x1+3x2<=300\n6/5x1+3/2x2=200\n-x1+4x2>=0", "max" | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Simplex Primal & Dual — Educational App") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| nvars = gr.Textbox(label="Número de variáveis", value="2") | |
| objective = gr.Textbox(label="Função objetivo", value="60,30") | |
| cons = gr.Textbox(label="Restrições", lines=6, | |
| value="2x1+3x2<=300\n6/5x1+3/2x2=200\n-x1+4x2>=0") | |
| sense = gr.Radio(["max","min"], value="max", label="Objetivo") | |
| run = gr.Button("Executar") | |
| with gr.Column(scale=1): | |
| model_out = gr.Textbox(label="Modelo", lines=8) | |
| primal_out = gr.Textbox(label="Primal", lines=20) | |
| dual_out = gr.Textbox(label="Dual", lines=20) | |
| run.click(run_algorithms, | |
| inputs=[nvars, objective, cons, sense], | |
| outputs=[model_out, primal_out, dual_out]) | |
| if __name__ == "__main__": | |
| demo.launch() | |
| """ | |
| """import gradio as gr | |
| from typing import List, Tuple, Dict, Any | |
| import math | |
| import re | |
| EPS = 1e-9 | |
| # --------------------------- Linear Algebra Helpers --------------------------- | |
| def zeros(rows, cols): | |
| return [[0.0] * cols for _ in range(rows)] | |
| # --------------------------- Parser Utilities --------------------------- | |
| def parse_coeffs(text: str) -> List[float]: | |
| # Parseia coeficientes da função objetivo. | |
| if not text or not text.strip(): | |
| return [] | |
| s = text.replace(',', ' ') | |
| parts = [p for p in s.split() if p.strip()] | |
| coeffs = [] | |
| for p in parts: | |
| try: | |
| coeffs.append(float(eval(p))) | |
| except Exception: | |
| raise ValueError(f"Coeficiente inválido: '{p}'") | |
| return coeffs | |
| def parse_constraints(text: str, nvars: int) -> List[Dict[str,Any]]: | |
| lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()] | |
| cons = [] | |
| for ln in lines: | |
| s = ln.replace(" ", "") | |
| # operador lógico | |
| if "<=" in s or "=<" in s: | |
| s = s.replace("=<", "<=") | |
| left, right = s.split("<=") | |
| sense = "<=" | |
| elif ">=" in s or "=>" in s: | |
| s = s.replace("=>", ">=") | |
| left, right = s.split(">=") | |
| sense = ">=" | |
| elif "=" in s: | |
| left, right = s.split("=") | |
| sense = "=" | |
| else: | |
| raise ValueError(f"Restrição inválida, faltando <=, >= ou = : '{ln}'") | |
| # RHS | |
| try: | |
| rhs = float(eval(right)) | |
| except: | |
| raise ValueError(f"RHS inválido: '{right}'") | |
| # capturar termos tipo +2x1, -3x2, 6/5x1 | |
| pattern = r'([+-]?[0-9./]*)(x[0-9]+)' | |
| terms = re.findall(pattern, left) | |
| coeffs = [0.0] * nvars | |
| for coef_str, var_str in terms: | |
| idx = int(var_str[1:]) - 1 | |
| if idx < 0 or idx >= nvars: | |
| raise ValueError(f"Variável fora do intervalo: {var_str}") | |
| if coef_str in ["", "+"]: | |
| v = 1.0 | |
| elif coef_str == "-": | |
| v = -1.0 | |
| else: | |
| v = float(eval(coef_str)) | |
| coeffs[idx] += v | |
| cons.append({ | |
| "coeffs": coeffs, | |
| "sense": sense, | |
| "rhs": rhs | |
| }) | |
| return cons | |
| # --------------------------- Simplex Implementation --------------------------- | |
| def build_canonical_tableau(objective: List[float], constraints: List[Dict[str,Any]], sense: str = 'max'): | |
| c = objective[:] | |
| obj_mult = 1.0 | |
| if sense == 'min': | |
| obj_mult = -1.0 | |
| c_adj = [ci * obj_mult for ci in c] | |
| norm = [] | |
| for row in constraints: | |
| coeffs = row['coeffs'][:] | |
| rhs = row['rhs'] | |
| s = row['sense'] | |
| if s == '>=': | |
| coeffs = [-a for a in coeffs] | |
| rhs = -rhs | |
| s = '<=' | |
| norm.append({"coeffs": coeffs, "rhs": rhs, "sense": s}) | |
| m = len(norm) | |
| n = len(c_adj) | |
| T = zeros(m + 1, n + m + 1) | |
| for i in range(m): | |
| for j in range(n): | |
| T[i][j] = norm[i]['coeffs'][j] | |
| T[i][n + i] = 1.0 | |
| T[i][n + m] = norm[i]['rhs'] | |
| for j in range(n): | |
| T[m][j] = -c_adj[j] | |
| return T, n, m, c, obj_mult, norm | |
| def extract_solution_from_tableau(T, basis, n, m, original_c): | |
| x = [0.0] * n | |
| for i in range(m): | |
| if basis[i] < n: | |
| x[basis[i]] = T[i][n + m] | |
| obj = sum(original_c[j] * x[j] for j in range(n)) | |
| return x, obj | |
| def snapshot(T, basis, n, m): | |
| x = [0.0] * n | |
| for i in range(m): | |
| if basis[i] < n: | |
| x[basis[i]] = T[i][n + m] | |
| return { | |
| "basis": basis[:], | |
| "x": [float(f"{v:.6f}") for v in x], | |
| "tableau": [row[:] for row in T] | |
| } | |
| def pivot(T, row, col): | |
| piv = T[row][col] | |
| if abs(piv) < EPS: | |
| raise ZeroDivisionError("Pivot quase zero") | |
| cols = len(T[0]) | |
| rows = len(T) | |
| for j in range(cols): | |
| T[row][j] /= piv | |
| for i in range(rows): | |
| if i == row: | |
| continue | |
| factor = T[i][col] | |
| for j in range(cols): | |
| T[i][j] -= factor * T[row][j] | |
| def primal_simplex(objective, constraints, sense): | |
| T, n, m, original_c, obj_mult, norm = build_canonical_tableau(objective, constraints, sense) | |
| basis = [n + i for i in range(m)] | |
| path = [snapshot(T, basis, n, m)] | |
| for _ in range(500): | |
| entering = next((j for j in range(n + m) if T[m][j] < -EPS), None) | |
| if entering is None: | |
| break | |
| best = math.inf | |
| leaving = None | |
| for i in range(m): | |
| a = T[i][entering] | |
| if a > EPS: | |
| r = T[i][n + m] / a | |
| if r < best: | |
| best = r | |
| leaving = i | |
| if leaving is None: | |
| return {"status": "unbounded", "path": path} | |
| pivot(T, leaving, entering) | |
| basis[leaving] = entering | |
| path.append(snapshot(T, basis, n, m)) | |
| x, obj = extract_solution_from_tableau(T, basis, n, m, original_c) | |
| reduced = [float(f"{(original_c[j] + T[m][j]):.6f}") for j in range(n)] | |
| shadow = [float(f"{(-T[m][n + i]):.6f}") for i in range(m)] | |
| return { | |
| "status": "optimal", | |
| "x": x, | |
| "obj": obj, | |
| "path": path, | |
| "reduced_costs": reduced, | |
| "shadow_prices": shadow | |
| } | |
| def dual_simplex(objective, constraints, sense): | |
| # usa mesma implementação — ok | |
| return primal_simplex(objective, constraints, sense) | |
| # --------------------------- Gradio App UI --------------------------- | |
| def run_algorithms(nvars_str, objective_str, cons_str, sense): | |
| try: | |
| nvars = int(nvars_str) | |
| objective = parse_coeffs(objective_str) | |
| constraints = parse_constraints(cons_str, nvars) | |
| if len(objective) != nvars: | |
| return "Erro: objetivo não bate com nvars", "", "" | |
| except Exception as e: | |
| return f"Erro ao ler entrada: {e}", "", "" | |
| primal = primal_simplex(objective, constraints, sense) | |
| dual = dual_simplex(objective, constraints, sense) | |
| def format_res(res): | |
| if res["status"] != "optimal": | |
| return f"Status: {res['status']}" | |
| out = [] | |
| out.append(f"Solução x* = {res['x']}") | |
| out.append(f"Valor objetivo = {res['obj']}") | |
| out.append(f"Preços-sombra = {res['shadow_prices']}") | |
| out.append(f"Custos reduzidos = {res['reduced_costs']}") | |
| out.append(f"\n--- Path ({len(res['path'])}) ---") | |
| for k, step in enumerate(res["path"]): | |
| out.append(f"\n### Passo {k+1}") | |
| out.append(f"Base = {step['basis']}") | |
| out.append(f"x = {step['x']}") | |
| out.append("Tableau:") | |
| for row in step["tableau"]: | |
| out.append(" " + " ".join(f"{v:8.3f}" for v in row)) | |
| return "\n".join(out) | |
| model_txt = "Modelo:\nObjetivo: " + str(objective) + "\nRestrições:\n" | |
| for c in constraints: | |
| model_txt += f"{c['coeffs']} {c['sense']} {c['rhs']}\n" | |
| return model_txt, format_res(primal), format_res(dual) | |
| def demo_input(): | |
| return "2", "60,30", "2x1+3x2<=300\n6/5x1+3/2x2=200\n-x1+4x2>=0", "max" | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Simplex Primal & Dual — Educational App") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| nvars = gr.Textbox(label="Número de variáveis", value="2") | |
| objective = gr.Textbox(label="Função objetivo", value="60,30") | |
| cons = gr.Textbox(label="Restrições", lines=6, | |
| value="2x1+3x2<=300\n6/5x1+3/2x2=200\n-x1+4x2>=0") | |
| sense = gr.Radio(["max","min"], value="max", label="Objetivo") | |
| run = gr.Button("Executar") | |
| with gr.Column(scale=1): | |
| model_out = gr.Textbox(label="Modelo", lines=8) | |
| primal_out = gr.Textbox(label="Primal", lines=20) | |
| dual_out = gr.Textbox(label="Dual", lines=20) | |
| run.click(run_algorithms, | |
| inputs=[nvars, objective, cons, sense], | |
| outputs=[model_out, primal_out, dual_out]) | |
| if __name__ == "__main__": | |
| demo.launch()""" |