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 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) # CORREÇÃO: regex robusto para coeficientes negativos ou omitidos term_pattern = r'([+-]?\d*(?:\.\d+)?)(x\d+)' 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 # agora os termos negativos funcionam corretamente terms = re.findall(term_pattern, left) for coef_str, var_str in terms: idx = int(var_str[1:]) - 1 # Trata coeficientes omitidos ou sinal puro if coef_str in ["", "+", None]: 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 = '' for i in range(tableau.shape[0]): html += '' for j in range(cols): val = tableau[i, j] html += f'' html += '' html += '
{val:.6g}
' 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---------------- 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'): #FASE 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 fase 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 #FASE 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 FASE 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 DUAL (GERAL) 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) # Reconstruir matriz A (somente colunas originais) e b, c (originais) A_orig = np.array([row['coeffs'] for row in constraints], dtype=float) # m x n_orig b_vec = np.array([row['rhs'] for row in constraints], dtype=float) cvec = np.array(c[:n_orig], dtype=float) # Construir matriz M das colunas que permaneceram no tableau (T_final[:m, :-1]) M = T_final[:m, :-1].copy() # m x ncols_keep # Basis matrix B (colunas básicas da fase II) — usar basis_final (índices em 0..ncols_keep-1) # Garantir que não haja None; se houver, já tentamos preencher antes. if any(bi is None for bi in basis_final): # Em casos degenerados, preencher com pseudo-solução: y zeros y_star = np.zeros(m) else: B = M[:, basis_final] # m x m # montar c_B (custos das colunas básicas) cB = np.zeros(m) for i, bi in enumerate(basis_final): if bi < len(c_full): cB[i] = c_full[bi] else: cB[i] = 0.0 # y^T = cB^T * B^{-1} try: Binv = np.linalg.inv(B) y_star = (cB @ Binv) # shape (m,) except np.linalg.LinAlgError: # fallback: tentar solução via least squares try: y_star, *_ = np.linalg.lstsq(B.T, cB, rcond=None) except Exception: y_star = np.zeros(m) # dual objective b^T y dual_obj = float(b_vec @ y_star) # Definir folgas/violação das desigualdades do dual dependendo do sentido primal # Se primal == 'max' -> dual é min b^T y s.t. A^T y >= c => slack = A^T y - c (>=0) # Se primal == 'min' -> dual is max b^T y s.t. A^T y <= c => slack = c - A^T y (>=0) if sense == 'max': dual_slacks = (A_orig.T @ y_star) - cvec else: dual_slacks = cvec - (A_orig.T @ y_star) # Preços-sombra (y) - ajustar sinal/convenção para exibição: mantemos y_star como calculado. shadow = [] for i, row in enumerate(constraints): shadow.append(round(float(y_star[i]), 8)) return { 'status': 'optimal', 'x': [round(v, 8) for v in x], 'obj': round(z, 8), 'y_dual': [round(float(v), 8) for v in y_star], 'dual_obj': round(dual_obj, 8), 'dual_slacks': [round(float(v), 8) for v in dual_slacks], 'A': A_orig.tolist(), 'b': b_vec.tolist(), 'c': cvec.tolist(), '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 # ---------------- 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 (Fase I obj = {res.get('phase1_obj')})", '', '', '', '' # Phase I failed if status == 'phase1_failed': return f"Erro na Fase I: {res.get('error','(sem detalhe)')}", '', '', '', '' 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', []) y_dual = res.get('y_dual', []) dual_obj = res.get('dual_obj', None) dual_slacks = res.get('dual_slacks', []) A = res.get('A', []) b = res.get('b', []) cvec = res.get('c', []) else: return f"Erro na resolução: status inesperado '{status}' - {res.get('error','')}", '', '', '', '' steps_html_phase2 = "" for idx, step in enumerate(path_primal): steps_html_phase2 += f"

Fase II — Passo {idx+1} — Base: {step.get('basis','?')}

" steps_html_phase2 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "
" steps_html_phase1 = "" for idx, step in enumerate(path_phase1): steps_html_phase1 += f"

Fase I — Passo {idx+1} — Base: {step.get('basis','?')}

" steps_html_phase1 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "
" 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"

Valor ótimo (estimado) = {z_primal:.6g}

" 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\n" summary += f"Solução dual y* = {y_dual}\n" summary += f"Valor dual b^T y = {dual_obj}\n" summary += f"Folgas/violação dual (A^T y - c) = {dual_slacks}\n\n" summary += f"Preços-sombra (dual interpretado) = {shadow}\n" summary += f"Custos reduzidos (vars originais) = {reduced}\n" return model_txt, solution_html, steps_html_phase1, steps_html_phase2, summary # ---------------- Gradio UI ---------------- with gr.Blocks() as demo: gr.Markdown("# Simplex — Duas Fases (Fase I / Fase II) (Dual Geral)") 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=12) 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]) 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)