Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -86,58 +86,6 @@ def parse_constraints(text: str, nvars: int) -> Tuple[List[Dict[str,Any]], List[
|
|
| 86 |
|
| 87 |
return cons, sorted(list(set(free_vars)))
|
| 88 |
|
| 89 |
-
"""def parse_constraints(text: str, nvars: int) -> Tuple[List[Dict[str,Any]], List[int]]:
|
| 90 |
-
lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()]
|
| 91 |
-
skip_words = ["tal que", "sujeito a", "subject to", "s.t.", "st:"]
|
| 92 |
-
lines = [ln for ln in lines if not any(word in ln.lower() for word in skip_words)]
|
| 93 |
-
|
| 94 |
-
free_vars = []
|
| 95 |
-
cons = []
|
| 96 |
-
pattern_free = re.compile(r'x([0-9]+)\s*(livre|free)', flags=re.I)
|
| 97 |
-
term_pattern = r'([+-]?[0-9./]*)(x[0-9]+)'
|
| 98 |
-
|
| 99 |
-
for ln in lines[:]:
|
| 100 |
-
m = pattern_free.search(ln)
|
| 101 |
-
if m:
|
| 102 |
-
idx = int(m.group(1)) - 1
|
| 103 |
-
if idx < 0 or idx >= nvars:
|
| 104 |
-
raise ValueError(f"Variável livre fora do intervalo: x{idx+1}")
|
| 105 |
-
free_vars.append(idx)
|
| 106 |
-
lines.remove(ln)
|
| 107 |
-
|
| 108 |
-
for ln in lines:
|
| 109 |
-
s = ln.replace(" ", "")
|
| 110 |
-
if "<=" in s or "=<" in s:
|
| 111 |
-
s = s.replace("=<", "<=")
|
| 112 |
-
left, right = s.split("<=")
|
| 113 |
-
sense = "<="
|
| 114 |
-
elif ">=" in s or "=>" in s:
|
| 115 |
-
s = s.replace("=>", ">=")
|
| 116 |
-
left, right = s.split(">=")
|
| 117 |
-
sense = ">="
|
| 118 |
-
elif "=" in s:
|
| 119 |
-
left, right = s.split("=")
|
| 120 |
-
sense = "="
|
| 121 |
-
else:
|
| 122 |
-
raise ValueError(f"Faltando <=, >= ou =: '{ln}'")
|
| 123 |
-
try:
|
| 124 |
-
rhs = float(eval(right))
|
| 125 |
-
except Exception:
|
| 126 |
-
raise ValueError(f"RHS inválido em: '{ln}'")
|
| 127 |
-
coeffs = [0.0] * nvars
|
| 128 |
-
terms = re.findall(term_pattern, left)
|
| 129 |
-
for coef_str, var_str in terms:
|
| 130 |
-
idx = int(var_str[1:]) - 1
|
| 131 |
-
if coef_str in ["", "+"]:
|
| 132 |
-
v = 1.0
|
| 133 |
-
elif coef_str == "-":
|
| 134 |
-
v = -1.0
|
| 135 |
-
else:
|
| 136 |
-
v = float(eval(coef_str))
|
| 137 |
-
coeffs[idx] += v
|
| 138 |
-
cons.append({'coeffs': coeffs, 'sense': sense, 'rhs': rhs})
|
| 139 |
-
return cons, sorted(list(set(free_vars)))"""
|
| 140 |
-
|
| 141 |
def expand_free_variables(nvars: int, c: List[float], constraints: List[Dict[str,Any]], free_vars: List[int]):
|
| 142 |
new_c = []
|
| 143 |
mapping = {}
|
|
@@ -278,12 +226,8 @@ def build_tableau_two_phase(c: List[float], constraints: List[Dict[str,Any]], se
|
|
| 278 |
return T, basis, (n, slacks, artificials), art_positions, c_adj
|
| 279 |
|
| 280 |
|
| 281 |
-
def run_two_phase(c, constraints, sense='max'):
|
| 282 |
-
|
| 283 |
-
#Implementação 100% por tableau — compatível com HuggingFace
|
| 284 |
-
#Phase I + Phase II completas (sem SciPy).
|
| 285 |
-
|
| 286 |
-
# ---------- PHASE I ----------
|
| 287 |
T0, basis0, (n_orig, n_slack, n_art), art_positions, c_adj = build_tableau_two_phase(
|
| 288 |
c, constraints, sense
|
| 289 |
)
|
|
@@ -313,7 +257,7 @@ def run_two_phase(c, constraints, sense='max'):
|
|
| 313 |
old_ncols = T1.shape[1] - 1
|
| 314 |
keep_cols = [j for j in range(old_ncols) if j not in art_cols]
|
| 315 |
|
| 316 |
-
# construir tableau da
|
| 317 |
T2 = np.zeros((T1.shape[0], len(keep_cols) + 1))
|
| 318 |
for i, col in enumerate(keep_cols):
|
| 319 |
T2[:, i] = T1[:, col]
|
|
@@ -348,7 +292,7 @@ def run_two_phase(c, constraints, sense='max'):
|
|
| 348 |
if not replaced:
|
| 349 |
basis2[i] = None
|
| 350 |
|
| 351 |
-
#
|
| 352 |
c_full = []
|
| 353 |
for col in keep_cols:
|
| 354 |
if col < len(c_adj):
|
|
@@ -376,7 +320,7 @@ def run_two_phase(c, constraints, sense='max'):
|
|
| 376 |
used.add(j)
|
| 377 |
break
|
| 378 |
|
| 379 |
-
#
|
| 380 |
try:
|
| 381 |
T_final, basis_final, path2 = primal_simplex_tableau(T2.copy(), basis2.copy())
|
| 382 |
except Exception as e:
|
|
@@ -387,7 +331,7 @@ def run_two_phase(c, constraints, sense='max'):
|
|
| 387 |
'trace': traceback.format_exc()
|
| 388 |
}
|
| 389 |
|
| 390 |
-
#
|
| 391 |
x = [0.0] * n_orig
|
| 392 |
|
| 393 |
for i, bi in enumerate(basis_final):
|
|
@@ -503,11 +447,11 @@ def run_algorithms(nvars_str, objective_str, cons_str, sense, mode):
|
|
| 503 |
status = res.get('status')
|
| 504 |
# infeasible detected in Phase I
|
| 505 |
if status == 'infeasible':
|
| 506 |
-
return f"Problema inviável (
|
| 507 |
|
| 508 |
# Phase I failed
|
| 509 |
if status == 'phase1_failed':
|
| 510 |
-
return f"Erro na
|
| 511 |
|
| 512 |
if status == 'optimal':
|
| 513 |
x_primal = res['x']
|
|
@@ -561,7 +505,7 @@ def run_algorithms(nvars_str, objective_str, cons_str, sense, mode):
|
|
| 561 |
# ---------------- Gradio UI ----------------
|
| 562 |
|
| 563 |
with gr.Blocks() as demo:
|
| 564 |
-
gr.Markdown("# Simplex — Duas Fases (
|
| 565 |
with gr.Row():
|
| 566 |
with gr.Column(scale=1):
|
| 567 |
nvars = gr.Textbox(label='Número de variáveis (n)', value='2')
|
|
@@ -583,505 +527,4 @@ with gr.Blocks() as demo:
|
|
| 583 |
|
| 584 |
if __name__ == '__main__':
|
| 585 |
demo.launch(ssr_mode=False)
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
"""import re
|
| 589 |
-
import traceback
|
| 590 |
-
from typing import List, Dict, Any, Tuple
|
| 591 |
-
import numpy as np
|
| 592 |
-
import pandas as pd
|
| 593 |
-
import gradio as gr
|
| 594 |
-
from fpdf import FPDF
|
| 595 |
-
|
| 596 |
-
EPS = 1e-9
|
| 597 |
-
|
| 598 |
-
# ---------------- Parsing utilities ----------------
|
| 599 |
-
|
| 600 |
-
def parse_coeffs(text: str) -> List[float]:
|
| 601 |
-
if not text or not text.strip():
|
| 602 |
-
return []
|
| 603 |
-
s = text.replace(',', ' ')
|
| 604 |
-
parts = [p for p in s.split() if p.strip()]
|
| 605 |
-
coeffs = []
|
| 606 |
-
for p in parts:
|
| 607 |
-
try:
|
| 608 |
-
coeffs.append(float(eval(p)))
|
| 609 |
-
except Exception:
|
| 610 |
-
raise ValueError(f"Coeficiente inválido: '{p}'")
|
| 611 |
-
return coeffs
|
| 612 |
-
|
| 613 |
-
def parse_constraints(text: str, nvars: int) -> Tuple[List[Dict[str,Any]], List[int]]:
|
| 614 |
-
lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()]
|
| 615 |
-
skip_words = ["tal que", "sujeito a", "subject to", "s.t.", "st:"]
|
| 616 |
-
lines = [ln for ln in lines if not any(word in ln.lower() for word in skip_words)]
|
| 617 |
-
|
| 618 |
-
free_vars = []
|
| 619 |
-
cons = []
|
| 620 |
-
pattern_free = re.compile(r'x([0-9]+)\s*(livre|free)', flags=re.I)
|
| 621 |
-
term_pattern = r'([+-]?[0-9./]*)(x[0-9]+)'
|
| 622 |
-
|
| 623 |
-
for ln in lines[:]:
|
| 624 |
-
m = pattern_free.search(ln)
|
| 625 |
-
if m:
|
| 626 |
-
idx = int(m.group(1)) - 1
|
| 627 |
-
if idx < 0 or idx >= nvars:
|
| 628 |
-
raise ValueError(f"Variável livre fora do intervalo: x{idx+1}")
|
| 629 |
-
free_vars.append(idx)
|
| 630 |
-
lines.remove(ln)
|
| 631 |
-
|
| 632 |
-
for ln in lines:
|
| 633 |
-
s = ln.replace(" ", "")
|
| 634 |
-
if "<=" in s or "=<" in s:
|
| 635 |
-
s = s.replace("=<", "<=")
|
| 636 |
-
left, right = s.split("<=")
|
| 637 |
-
sense = "<="
|
| 638 |
-
elif ">=" in s or "=>" in s:
|
| 639 |
-
s = s.replace("=>", ">=")
|
| 640 |
-
left, right = s.split(">=")
|
| 641 |
-
sense = ">="
|
| 642 |
-
elif "=" in s:
|
| 643 |
-
left, right = s.split("=")
|
| 644 |
-
sense = "="
|
| 645 |
-
else:
|
| 646 |
-
raise ValueError(f"Faltando <=, >= ou =: '{ln}'")
|
| 647 |
-
try:
|
| 648 |
-
rhs = float(eval(right))
|
| 649 |
-
except Exception:
|
| 650 |
-
raise ValueError(f"RHS inválido em: '{ln}'")
|
| 651 |
-
coeffs = [0.0] * nvars
|
| 652 |
-
terms = re.findall(term_pattern, left)
|
| 653 |
-
for coef_str, var_str in terms:
|
| 654 |
-
idx = int(var_str[1:]) - 1
|
| 655 |
-
if coef_str in ["", "+"]:
|
| 656 |
-
v = 1.0
|
| 657 |
-
elif coef_str == "-":
|
| 658 |
-
v = -1.0
|
| 659 |
-
else:
|
| 660 |
-
v = float(eval(coef_str))
|
| 661 |
-
coeffs[idx] += v
|
| 662 |
-
cons.append({'coeffs': coeffs, 'sense': sense, 'rhs': rhs})
|
| 663 |
-
return cons, sorted(list(set(free_vars)))
|
| 664 |
-
|
| 665 |
-
def expand_free_variables(nvars: int, c: List[float], constraints: List[Dict[str,Any]], free_vars: List[int]):
|
| 666 |
-
new_c = []
|
| 667 |
-
mapping = {}
|
| 668 |
-
for i in range(nvars):
|
| 669 |
-
if i in free_vars:
|
| 670 |
-
new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1)
|
| 671 |
-
new_c.append(-c[i]); mapping[len(new_c)-1] = (i, -1)
|
| 672 |
-
else:
|
| 673 |
-
new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1)
|
| 674 |
-
new_constraints = []
|
| 675 |
-
for row in constraints:
|
| 676 |
-
coeffs = row['coeffs']
|
| 677 |
-
new_coeffs = []
|
| 678 |
-
for i in range(nvars):
|
| 679 |
-
if i in free_vars:
|
| 680 |
-
new_coeffs.append(coeffs[i])
|
| 681 |
-
new_coeffs.append(-coeffs[i])
|
| 682 |
-
else:
|
| 683 |
-
new_coeffs.append(coeffs[i])
|
| 684 |
-
new_constraints.append({'coeffs': new_coeffs, 'sense': row['sense'], 'rhs': row['rhs']})
|
| 685 |
-
return len(new_c), new_c, new_constraints, mapping
|
| 686 |
-
|
| 687 |
-
# ---------------- Tableau helpers ----------------
|
| 688 |
-
|
| 689 |
-
def snapshot_html(tableau: np.ndarray, basis: List[int]) -> str:
|
| 690 |
-
cols = tableau.shape[1]
|
| 691 |
-
html = '<table border="1" style="border-collapse:collapse;font-family:Arial; font-size:12px;">'
|
| 692 |
-
for i in range(tableau.shape[0]):
|
| 693 |
-
html += '<tr>'
|
| 694 |
-
for j in range(cols):
|
| 695 |
-
val = tableau[i, j]
|
| 696 |
-
html += f'<td style="padding:4px;">{val:.6g}</td>'
|
| 697 |
-
html += '</tr>'
|
| 698 |
-
html += '</table>'
|
| 699 |
-
return html
|
| 700 |
-
|
| 701 |
-
def primal_simplex_tableau(T: np.ndarray, basis: List[int], max_iters=1000) -> Tuple[np.ndarray, List[int], List[Dict[str,Any]]]:
|
| 702 |
-
m = T.shape[0] - 1
|
| 703 |
-
ncols = T.shape[1]
|
| 704 |
-
path = []
|
| 705 |
-
path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)})
|
| 706 |
-
|
| 707 |
-
it = 0
|
| 708 |
-
while it < max_iters:
|
| 709 |
-
it += 1
|
| 710 |
-
obj_row = T[-1, :-1]
|
| 711 |
-
entering_candidates = np.where(obj_row < -EPS)[0]
|
| 712 |
-
if entering_candidates.size == 0:
|
| 713 |
-
break
|
| 714 |
-
entering = int(entering_candidates[0])
|
| 715 |
-
ratios = np.full(m, np.inf)
|
| 716 |
-
for i in range(m):
|
| 717 |
-
a = T[i, entering]
|
| 718 |
-
if a > EPS:
|
| 719 |
-
ratios[i] = T[i, -1] / a
|
| 720 |
-
if np.all(np.isinf(ratios)):
|
| 721 |
-
raise Exception('Unbounded LP')
|
| 722 |
-
leaving = int(np.argmin(ratios))
|
| 723 |
-
piv = T[leaving, entering]
|
| 724 |
-
T[leaving, :] = T[leaving, :] / piv
|
| 725 |
-
for i in range(m+1):
|
| 726 |
-
if i == leaving: continue
|
| 727 |
-
T[i, :] = T[i, :] - T[i, entering] * T[leaving, :]
|
| 728 |
-
basis[leaving] = entering
|
| 729 |
-
path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)})
|
| 730 |
-
return T, basis, path
|
| 731 |
-
|
| 732 |
-
# ---------------- Two-Phase implementation (CORRIGIDA) ----------------
|
| 733 |
-
|
| 734 |
-
def build_tableau_two_phase(c: List[float], constraints: List[Dict[str,Any]], sense: str = 'max'):
|
| 735 |
-
obj_mult = 1.0
|
| 736 |
-
if sense == 'min':
|
| 737 |
-
obj_mult = -1.0
|
| 738 |
-
c_adj = [ci * obj_mult for ci in c]
|
| 739 |
-
|
| 740 |
-
n = len(c_adj)
|
| 741 |
-
m = len(constraints)
|
| 742 |
-
|
| 743 |
-
slacks = 0
|
| 744 |
-
artificials = 0
|
| 745 |
-
for row in constraints:
|
| 746 |
-
if row['sense'] == '<=':
|
| 747 |
-
slacks += 1
|
| 748 |
-
elif row['sense'] == '>=':
|
| 749 |
-
slacks += 1
|
| 750 |
-
artificials += 1
|
| 751 |
-
else:
|
| 752 |
-
artificials += 1
|
| 753 |
-
|
| 754 |
-
total_cols = n + slacks + artificials + 1
|
| 755 |
-
T = np.zeros((m + 1, total_cols))
|
| 756 |
-
|
| 757 |
-
slack_idx = n
|
| 758 |
-
artificial_idx = n + slacks
|
| 759 |
-
|
| 760 |
-
basis = []
|
| 761 |
-
art_positions = []
|
| 762 |
-
s_counter = 0
|
| 763 |
-
a_counter = 0
|
| 764 |
-
|
| 765 |
-
for i, row in enumerate(constraints):
|
| 766 |
-
coeffs = row['coeffs']
|
| 767 |
-
T[i, :n] = coeffs
|
| 768 |
-
if row['sense'] == '<=':
|
| 769 |
-
T[i, slack_idx + s_counter] = 1.0
|
| 770 |
-
basis.append(slack_idx + s_counter)
|
| 771 |
-
s_counter += 1
|
| 772 |
-
elif row['sense'] == '>=':
|
| 773 |
-
T[i, slack_idx + s_counter] = -1.0
|
| 774 |
-
T[i, artificial_idx + a_counter] = 1.0
|
| 775 |
-
basis.append(artificial_idx + a_counter)
|
| 776 |
-
art_positions.append(artificial_idx + a_counter)
|
| 777 |
-
s_counter += 1
|
| 778 |
-
a_counter += 1
|
| 779 |
-
else: # equality
|
| 780 |
-
T[i, artificial_idx + a_counter] = 1.0
|
| 781 |
-
basis.append(artificial_idx + a_counter)
|
| 782 |
-
art_positions.append(artificial_idx + a_counter)
|
| 783 |
-
a_counter += 1
|
| 784 |
-
T[i, -1] = row['rhs']
|
| 785 |
-
|
| 786 |
-
# Phase I objective: minimize sum of artificials.
|
| 787 |
-
# Convert to maximization for our tableau solver: maximize (-sum a_j)
|
| 788 |
-
# So c_phase1 (for maximization) = -1 for each artificial column.
|
| 789 |
-
c_phase1 = np.zeros(total_cols - 1)
|
| 790 |
-
for a in art_positions:
|
| 791 |
-
c_phase1[a] = -1.0
|
| 792 |
-
|
| 793 |
-
# In tableau we store -c in last row, so set T[-1, :-1] = -c_phase1
|
| 794 |
-
T[-1, :-1] = -c_phase1
|
| 795 |
-
|
| 796 |
-
# But because artificials are in basis, we must adjust objective row:
|
| 797 |
-
# T[-1, :] = -c + sum_{i in basis} c_Bi * row_i, where c_Bi = c_phase1[basis_i]
|
| 798 |
-
for i in range(m):
|
| 799 |
-
bi = basis[i]
|
| 800 |
-
cBi = c_phase1[bi] if bi < len(c_phase1) else 0.0
|
| 801 |
-
if abs(cBi) > EPS:
|
| 802 |
-
T[-1, :] += cBi * T[i, :]
|
| 803 |
-
|
| 804 |
-
return T, basis, (n, slacks, artificials), art_positions, c_adj
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
def run_two_phase(c, constraints, sense='max'):
|
| 808 |
-
|
| 809 |
-
#Implementação 100% por tableau — compatível com HuggingFace
|
| 810 |
-
#Phase I + Phase II completas (sem SciPy).
|
| 811 |
-
|
| 812 |
-
# ---------- PHASE I ----------
|
| 813 |
-
T0, basis0, (n_orig, n_slack, n_art), art_positions, c_adj = build_tableau_two_phase(
|
| 814 |
-
c, constraints, sense
|
| 815 |
-
)
|
| 816 |
-
|
| 817 |
-
try:
|
| 818 |
-
T1, basis1, path1 = primal_simplex_tableau(T0.copy(), basis0.copy())
|
| 819 |
-
except Exception as e:
|
| 820 |
-
return {
|
| 821 |
-
'status': 'phase1_failed',
|
| 822 |
-
'error': str(e),
|
| 823 |
-
'trace': traceback.format_exc()
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
phase1_obj = float(T1[-1, -1])
|
| 827 |
-
|
| 828 |
-
# se sum(a_j) != 0 → inviável
|
| 829 |
-
if abs(phase1_obj) > 1e-6:
|
| 830 |
-
return {
|
| 831 |
-
'status': 'infeasible',
|
| 832 |
-
'phase1_obj': phase1_obj,
|
| 833 |
-
'phase1_path': path1,
|
| 834 |
-
'tableau_phase1': T1
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
# ---------- REMOVER ARTIFICIAIS ----------
|
| 838 |
-
art_cols = set(art_positions)
|
| 839 |
-
old_ncols = T1.shape[1] - 1
|
| 840 |
-
keep_cols = [j for j in range(old_ncols) if j not in art_cols]
|
| 841 |
-
|
| 842 |
-
# construir tableau da Phase II (T2)
|
| 843 |
-
T2 = np.zeros((T1.shape[0], len(keep_cols) + 1))
|
| 844 |
-
for i, col in enumerate(keep_cols):
|
| 845 |
-
T2[:, i] = T1[:, col]
|
| 846 |
-
T2[:, -1] = T1[:, -1]
|
| 847 |
-
|
| 848 |
-
# nova base
|
| 849 |
-
basis2 = []
|
| 850 |
-
for bi in basis1:
|
| 851 |
-
if bi in art_cols:
|
| 852 |
-
basis2.append(None)
|
| 853 |
-
else:
|
| 854 |
-
basis2.append(keep_cols.index(bi))
|
| 855 |
-
|
| 856 |
-
# corrigir linhas onde a base ficou None
|
| 857 |
-
used = set([b for b in basis2 if b is not None])
|
| 858 |
-
m = T2.shape[0] - 1
|
| 859 |
-
|
| 860 |
-
for i in range(m):
|
| 861 |
-
if basis2[i] is None:
|
| 862 |
-
replaced = False
|
| 863 |
-
for j in range(T2.shape[1] - 1):
|
| 864 |
-
if j not in used and abs(T2[i, j]) > EPS:
|
| 865 |
-
piv = T2[i, j]
|
| 866 |
-
T2[i, :] = T2[i, :] / piv
|
| 867 |
-
for r in range(m+1):
|
| 868 |
-
if r != i:
|
| 869 |
-
T2[r, :] -= T2[r, j] * T2[i, :]
|
| 870 |
-
basis2[i] = j
|
| 871 |
-
used.add(j)
|
| 872 |
-
replaced = True
|
| 873 |
-
break
|
| 874 |
-
if not replaced:
|
| 875 |
-
basis2[i] = None
|
| 876 |
-
|
| 877 |
-
# ---------- PHASE II — definir objetivo original ----------
|
| 878 |
-
c_full = []
|
| 879 |
-
for col in keep_cols:
|
| 880 |
-
if col < len(c_adj):
|
| 881 |
-
c_full.append(c_adj[col])
|
| 882 |
-
else:
|
| 883 |
-
c_full.append(0.0)
|
| 884 |
-
|
| 885 |
-
c_full = np.array(c_full)
|
| 886 |
-
|
| 887 |
-
T2[-1, :-1] = -c_full
|
| 888 |
-
|
| 889 |
-
for i in range(m):
|
| 890 |
-
bi = basis2[i]
|
| 891 |
-
if bi is not None and bi < len(c_full):
|
| 892 |
-
coef = c_full[bi]
|
| 893 |
-
if abs(coef) > EPS:
|
| 894 |
-
T2[-1, :] += coef * T2[i, :]
|
| 895 |
-
|
| 896 |
-
# preencher bases ausentes
|
| 897 |
-
for i in range(m):
|
| 898 |
-
if basis2[i] is None:
|
| 899 |
-
for j in range(T2.shape[1]-1):
|
| 900 |
-
if j not in used:
|
| 901 |
-
basis2[i] = j
|
| 902 |
-
used.add(j)
|
| 903 |
-
break
|
| 904 |
-
|
| 905 |
-
# ---------- SIMPLEX PHASE II ----------
|
| 906 |
-
try:
|
| 907 |
-
T_final, basis_final, path2 = primal_simplex_tableau(T2.copy(), basis2.copy())
|
| 908 |
-
except Exception as e:
|
| 909 |
-
return {
|
| 910 |
-
'status': 'phase2_failed',
|
| 911 |
-
'error': str(e),
|
| 912 |
-
'phase1_path': path1,
|
| 913 |
-
'trace': traceback.format_exc()
|
| 914 |
-
}
|
| 915 |
-
|
| 916 |
-
# ---------- EXTRAI X*, REDUCED COSTS E SHADOW PRICES ----------
|
| 917 |
-
x = [0.0] * n_orig
|
| 918 |
-
|
| 919 |
-
for i, bi in enumerate(basis_final):
|
| 920 |
-
if bi is not None:
|
| 921 |
-
oldcol = keep_cols[bi]
|
| 922 |
-
if oldcol < n_orig:
|
| 923 |
-
x[oldcol] = float(T_final[i, -1])
|
| 924 |
-
|
| 925 |
-
z = float(T_final[-1, -1])
|
| 926 |
-
|
| 927 |
-
# custos reduzidos apenas variáveis originais
|
| 928 |
-
reduced = []
|
| 929 |
-
for j in range(n_orig):
|
| 930 |
-
if j in keep_cols:
|
| 931 |
-
colpos = keep_cols.index(j)
|
| 932 |
-
z_j = -T_final[-1, colpos]
|
| 933 |
-
reduced.append(round(c_adj[j] - z_j, 8))
|
| 934 |
-
else:
|
| 935 |
-
reduced.append(0.0)
|
| 936 |
-
|
| 937 |
-
# preços-sombra = coeficientes de slack na linha da função objetivo
|
| 938 |
-
shadow = []
|
| 939 |
-
for i in range(len(constraints)):
|
| 940 |
-
col = n_orig + i
|
| 941 |
-
if col in keep_cols:
|
| 942 |
-
idx = keep_cols.index(col)
|
| 943 |
-
shadow.append(round(-T_final[-1, idx], 8))
|
| 944 |
-
else:
|
| 945 |
-
shadow.append(0.0)
|
| 946 |
-
|
| 947 |
-
return {
|
| 948 |
-
'status': 'optimal',
|
| 949 |
-
'x': [round(v, 8) for v in x],
|
| 950 |
-
'obj': round(z, 8),
|
| 951 |
-
'path_phase1': path1,
|
| 952 |
-
'path_phase2': path2,
|
| 953 |
-
'tableau_final': T_final,
|
| 954 |
-
'basis_final': basis_final,
|
| 955 |
-
'reduced_costs': reduced,
|
| 956 |
-
'shadow_prices': shadow
|
| 957 |
-
}
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
# ---------------- Helpers & PDF ----------------
|
| 961 |
-
|
| 962 |
-
def clean_vector(vec):
|
| 963 |
-
try:
|
| 964 |
-
return [float(v) for v in vec]
|
| 965 |
-
except:
|
| 966 |
-
return vec
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
# ---------------- Gradio handler ----------------
|
| 970 |
-
|
| 971 |
-
def run_algorithms(nvars_str, objective_str, cons_str, sense, mode):
|
| 972 |
-
try:
|
| 973 |
-
nvars = int(nvars_str)
|
| 974 |
-
if nvars <= 0:
|
| 975 |
-
return 'Erro: nvars deve ser inteiro positivo', '', '', '', ''
|
| 976 |
-
c = parse_coeffs(objective_str)
|
| 977 |
-
if len(c) != nvars:
|
| 978 |
-
return 'Erro: coeficientes do objetivo não correspondem a nvars', '', '', '', ''
|
| 979 |
-
constraints, free_vars = parse_constraints(cons_str, nvars)
|
| 980 |
-
if free_vars:
|
| 981 |
-
nvars, c, constraints, mapping = expand_free_variables(nvars, c, constraints, free_vars)
|
| 982 |
-
except Exception as e:
|
| 983 |
-
return f'Erro ao ler entrada: {e}', '', '', '', ''
|
| 984 |
-
|
| 985 |
-
res = run_two_phase(c, constraints, sense)
|
| 986 |
-
status = res.get('status')
|
| 987 |
-
# infeasible detected in Phase I
|
| 988 |
-
if status == 'infeasible':
|
| 989 |
-
return f"Problema inviável (Phase I obj = {res.get('phase1_obj')})", '', '', '', ''
|
| 990 |
-
|
| 991 |
-
# Phase I failed
|
| 992 |
-
if status == 'phase1_failed':
|
| 993 |
-
return f"Erro na Phase I: {res.get('error','(sem detalhe)')}", '', '', '', ''
|
| 994 |
-
|
| 995 |
-
# Phase II via tableau succeeded (our run_two_phase returns 'optimal' in that case)
|
| 996 |
-
if status == 'optimal':
|
| 997 |
-
x_primal = res['x']
|
| 998 |
-
z_primal = res['obj']
|
| 999 |
-
reduced = res.get('reduced_costs', [])
|
| 1000 |
-
shadow = res.get('shadow_prices', [])
|
| 1001 |
-
T_final = res.get('tableau_final', None)
|
| 1002 |
-
path_primal = res.get('path_phase2', [])
|
| 1003 |
-
path_phase1 = res.get('path_phase1', [])
|
| 1004 |
-
|
| 1005 |
-
# Phase II solved via scipy.linprog successfully
|
| 1006 |
-
elif status == 'optimal_linprog':
|
| 1007 |
-
x_primal = res.get('x', [])
|
| 1008 |
-
z_primal = res.get('obj', None)
|
| 1009 |
-
# reduced costs / shadow prices may not be available from linprog
|
| 1010 |
-
reduced = res.get('reduced_costs', [])
|
| 1011 |
-
shadow = res.get('shadow_prices', [])
|
| 1012 |
-
T_final = None
|
| 1013 |
-
path_primal = res.get('phase1_path', []) # we still have Phase I path
|
| 1014 |
-
path_phase1 = res.get('phase1_path', [])
|
| 1015 |
-
|
| 1016 |
-
elif status in ('linprog_failed', 'no_scipy_or_error'):
|
| 1017 |
-
msg = res.get('linprog_message') or res.get('error') or 'linprog falhou (detalhes indisponíveis)'
|
| 1018 |
-
phase1_path = res.get('phase1_path', [])
|
| 1019 |
-
return f"Erro na resolução: {status} - {msg}", '', '', '', ''
|
| 1020 |
-
else:
|
| 1021 |
-
# fallback catch-all
|
| 1022 |
-
return f"Erro na resolução: status inesperado '{status}' - {res.get('error','')}", '', '', '', ''
|
| 1023 |
-
|
| 1024 |
-
x_primal = res['x']; z_primal = res['obj']
|
| 1025 |
-
reduced = res['reduced_costs']; shadow = res['shadow_prices']
|
| 1026 |
-
T_final = res['tableau_final']
|
| 1027 |
-
path2 = res['path_phase2']; path1 = res['path_phase1']
|
| 1028 |
-
|
| 1029 |
-
steps_html_phase2 = ""
|
| 1030 |
-
for idx, step in enumerate(path2):
|
| 1031 |
-
# step is dict with 'tableau' and 'basis'
|
| 1032 |
-
steps_html_phase2 += f"<h4>Phase II — Passo {idx+1} — Base: {step.get('basis','?')}</h4>"
|
| 1033 |
-
steps_html_phase2 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>"
|
| 1034 |
-
|
| 1035 |
-
steps_html_phase1 = ""
|
| 1036 |
-
for idx, step in enumerate(path1):
|
| 1037 |
-
steps_html_phase1 += f"<h4>Phase I — Passo {idx+1} — Base: {step.get('basis','?')}</h4>"
|
| 1038 |
-
steps_html_phase1 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>"
|
| 1039 |
-
|
| 1040 |
-
df = pd.DataFrame({'Variável': [f'x{i+1}' for i in range(len(x_primal))], 'Valor': x_primal})
|
| 1041 |
-
solution_html = df.to_html(index=False)
|
| 1042 |
-
solution_html += f"<p><b>Valor ótimo (estimado) = {z_primal:.6g}</b></p>"
|
| 1043 |
-
|
| 1044 |
-
x_primal = clean_vector(x_primal); reduced = clean_vector(reduced); shadow = clean_vector(shadow)
|
| 1045 |
-
z_primal = float(z_primal)
|
| 1046 |
-
|
| 1047 |
-
model_txt = f"Objective ({'min' if sense=='min' else 'max'}): {c}\nConstraints:\n"
|
| 1048 |
-
for r in constraints:
|
| 1049 |
-
model_txt += f" {r['coeffs']} {r['sense']} {r['rhs']}\n"
|
| 1050 |
-
|
| 1051 |
-
summary = ""
|
| 1052 |
-
summary += f"Solução primal x* = {x_primal}\n"
|
| 1053 |
-
summary += f"Z_primal (estimado) = {z_primal:.6g}\n"
|
| 1054 |
-
summary += f"Preços-sombra (dual estimado) = {shadow}\n"
|
| 1055 |
-
summary += f"Custos reduzidos (orig vars) = {reduced}\n"
|
| 1056 |
-
|
| 1057 |
-
return model_txt, solution_html, steps_html_phase2, steps_html_phase1, summary
|
| 1058 |
-
|
| 1059 |
-
# ---------------- Gradio UI ----------------
|
| 1060 |
-
|
| 1061 |
-
with gr.Blocks() as demo:
|
| 1062 |
-
gr.Markdown("# Simplex — Duas Fases (Phase I / Phase II) — Educational")
|
| 1063 |
-
with gr.Row():
|
| 1064 |
-
with gr.Column(scale=1):
|
| 1065 |
-
nvars = gr.Textbox(label='Número de variáveis (n)', value='2')
|
| 1066 |
-
objective = gr.Textbox(label='Coeficientes da função objetivo (ex: \"60 30\")', value='60 30')
|
| 1067 |
-
cons = gr.Textbox(label='Restrições (uma por linha). Ex.: 2x1 + 3x2 <= 300', lines=6,
|
| 1068 |
-
value='2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50')
|
| 1069 |
-
sense = gr.Radio(['max','min'], value='max', label='Tipo de objetivo')
|
| 1070 |
-
run = gr.Button('Executar Simplex (Duas Fases)')
|
| 1071 |
-
with gr.Column(scale=2):
|
| 1072 |
-
model_out = gr.Textbox(label='Função objetivo e restrições (modelo)', lines=6)
|
| 1073 |
-
solution_out = gr.HTML(label='Solução ótima (tabela)')
|
| 1074 |
-
steps_phase2_out = gr.HTML(label='Passos do Simplex (Phase II tableaus)')
|
| 1075 |
-
steps_phase1_out = gr.HTML(label='Passos do Simplex (Phase I tableaus)')
|
| 1076 |
-
summary_out = gr.Textbox(label='Resumo', lines=8)
|
| 1077 |
-
|
| 1078 |
-
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])
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
gr.Examples(examples=[["2","60 30","2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50","max"]], inputs=[nvars, objective, cons, sense])
|
| 1083 |
-
|
| 1084 |
-
if __name__ == '__main__':
|
| 1085 |
-
demo.launch(ssr_mode=False)"""
|
| 1086 |
-
|
| 1087 |
-
|
|
|
|
| 86 |
|
| 87 |
return cons, sorted(list(set(free_vars)))
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
def expand_free_variables(nvars: int, c: List[float], constraints: List[Dict[str,Any]], free_vars: List[int]):
|
| 90 |
new_c = []
|
| 91 |
mapping = {}
|
|
|
|
| 226 |
return T, basis, (n, slacks, artificials), art_positions, c_adj
|
| 227 |
|
| 228 |
|
| 229 |
+
def run_two_phase(c, constraints, sense='max'):
|
| 230 |
+
#FASE I
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
T0, basis0, (n_orig, n_slack, n_art), art_positions, c_adj = build_tableau_two_phase(
|
| 232 |
c, constraints, sense
|
| 233 |
)
|
|
|
|
| 257 |
old_ncols = T1.shape[1] - 1
|
| 258 |
keep_cols = [j for j in range(old_ncols) if j not in art_cols]
|
| 259 |
|
| 260 |
+
# construir tableau da fase II (T2)
|
| 261 |
T2 = np.zeros((T1.shape[0], len(keep_cols) + 1))
|
| 262 |
for i, col in enumerate(keep_cols):
|
| 263 |
T2[:, i] = T1[:, col]
|
|
|
|
| 292 |
if not replaced:
|
| 293 |
basis2[i] = None
|
| 294 |
|
| 295 |
+
#FASE II — definir objetivo original
|
| 296 |
c_full = []
|
| 297 |
for col in keep_cols:
|
| 298 |
if col < len(c_adj):
|
|
|
|
| 320 |
used.add(j)
|
| 321 |
break
|
| 322 |
|
| 323 |
+
#SIMPLEX FASE II
|
| 324 |
try:
|
| 325 |
T_final, basis_final, path2 = primal_simplex_tableau(T2.copy(), basis2.copy())
|
| 326 |
except Exception as e:
|
|
|
|
| 331 |
'trace': traceback.format_exc()
|
| 332 |
}
|
| 333 |
|
| 334 |
+
#EXTRAI X*, REDUCED COSTS E DUAL (GERAL)
|
| 335 |
x = [0.0] * n_orig
|
| 336 |
|
| 337 |
for i, bi in enumerate(basis_final):
|
|
|
|
| 447 |
status = res.get('status')
|
| 448 |
# infeasible detected in Phase I
|
| 449 |
if status == 'infeasible':
|
| 450 |
+
return f"Problema inviável (Fase I obj = {res.get('phase1_obj')})", '', '', '', ''
|
| 451 |
|
| 452 |
# Phase I failed
|
| 453 |
if status == 'phase1_failed':
|
| 454 |
+
return f"Erro na Fase I: {res.get('error','(sem detalhe)')}", '', '', '', ''
|
| 455 |
|
| 456 |
if status == 'optimal':
|
| 457 |
x_primal = res['x']
|
|
|
|
| 505 |
# ---------------- Gradio UI ----------------
|
| 506 |
|
| 507 |
with gr.Blocks() as demo:
|
| 508 |
+
gr.Markdown("# Simplex — Duas Fases (Fase I / Fase II) (Dual Geral)")
|
| 509 |
with gr.Row():
|
| 510 |
with gr.Column(scale=1):
|
| 511 |
nvars = gr.Textbox(label='Número de variáveis (n)', value='2')
|
|
|
|
| 527 |
|
| 528 |
if __name__ == '__main__':
|
| 529 |
demo.launch(ssr_mode=False)
|
| 530 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|