Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import re
|
| 2 |
import traceback
|
| 3 |
from typing import List, Dict, Any, Tuple
|
| 4 |
import numpy as np
|
|
@@ -492,6 +492,534 @@ with gr.Blocks() as demo:
|
|
| 492 |
|
| 493 |
|
| 494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
gr.Examples(examples=[["2","60 30","2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50","max"]], inputs=[nvars, objective, cons, sense])
|
| 496 |
|
| 497 |
if __name__ == '__main__':
|
|
|
|
| 1 |
+
"""import re
|
| 2 |
import traceback
|
| 3 |
from typing import List, Dict, Any, Tuple
|
| 4 |
import numpy as np
|
|
|
|
| 492 |
|
| 493 |
|
| 494 |
|
| 495 |
+
gr.Examples(examples=[["2","60 30","2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50","max"]], inputs=[nvars, objective, cons, sense])
|
| 496 |
+
|
| 497 |
+
if __name__ == '__main__':
|
| 498 |
+
demo.launch(ssr_mode=False)"""
|
| 499 |
+
import re
|
| 500 |
+
import traceback
|
| 501 |
+
from typing import List, Dict, Any, Tuple
|
| 502 |
+
import numpy as np
|
| 503 |
+
import pandas as pd
|
| 504 |
+
import gradio as gr
|
| 505 |
+
from fpdf import FPDF
|
| 506 |
+
|
| 507 |
+
EPS = 1e-9
|
| 508 |
+
|
| 509 |
+
# ---------------- Parsing utilities ----------------
|
| 510 |
+
|
| 511 |
+
def parse_coeffs(text: str) -> List[float]:
|
| 512 |
+
if not text or not text.strip():
|
| 513 |
+
return []
|
| 514 |
+
s = text.replace(',', ' ')
|
| 515 |
+
parts = [p for p in s.split() if p.strip()]
|
| 516 |
+
coeffs = []
|
| 517 |
+
for p in parts:
|
| 518 |
+
try:
|
| 519 |
+
coeffs.append(float(eval(p)))
|
| 520 |
+
except Exception:
|
| 521 |
+
raise ValueError(f"Coeficiente inválido: '{p}'")
|
| 522 |
+
return coeffs
|
| 523 |
+
|
| 524 |
+
def parse_constraints(text: str, nvars: int) -> Tuple[List[Dict[str,Any]], List[int]]:
|
| 525 |
+
lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()]
|
| 526 |
+
skip_words = ["tal que", "sujeito a", "subject to", "s.t.", "st:"]
|
| 527 |
+
lines = [ln for ln in lines if not any(word in ln.lower() for word in skip_words)]
|
| 528 |
+
|
| 529 |
+
free_vars = []
|
| 530 |
+
cons = []
|
| 531 |
+
pattern_free = re.compile(r'x([0-9]+)\s*(livre|free)', flags=re.I)
|
| 532 |
+
term_pattern = r'([+-]?[0-9./]*)(x[0-9]+)'
|
| 533 |
+
|
| 534 |
+
for ln in lines[:]:
|
| 535 |
+
m = pattern_free.search(ln)
|
| 536 |
+
if m:
|
| 537 |
+
idx = int(m.group(1)) - 1
|
| 538 |
+
if idx < 0 or idx >= nvars:
|
| 539 |
+
raise ValueError(f"Variável livre fora do intervalo: x{idx+1}")
|
| 540 |
+
free_vars.append(idx)
|
| 541 |
+
lines.remove(ln)
|
| 542 |
+
|
| 543 |
+
for ln in lines:
|
| 544 |
+
s = ln.replace(" ", "")
|
| 545 |
+
if "<=" in s or "=<" in s:
|
| 546 |
+
s = s.replace("=<", "<=")
|
| 547 |
+
left, right = s.split("<=")
|
| 548 |
+
sense = "<="
|
| 549 |
+
elif ">=" in s or "=>" in s:
|
| 550 |
+
s = s.replace("=>", ">=")
|
| 551 |
+
left, right = s.split(">=")
|
| 552 |
+
sense = ">="
|
| 553 |
+
elif "=" in s:
|
| 554 |
+
left, right = s.split("=")
|
| 555 |
+
sense = "="
|
| 556 |
+
else:
|
| 557 |
+
raise ValueError(f"Faltando <=, >= ou =: '{ln}'")
|
| 558 |
+
try:
|
| 559 |
+
rhs = float(eval(right))
|
| 560 |
+
except Exception:
|
| 561 |
+
raise ValueError(f"RHS inválido em: '{ln}'")
|
| 562 |
+
coeffs = [0.0] * nvars
|
| 563 |
+
terms = re.findall(term_pattern, left)
|
| 564 |
+
for coef_str, var_str in terms:
|
| 565 |
+
idx = int(var_str[1:]) - 1
|
| 566 |
+
if coef_str in ["", "+"]:
|
| 567 |
+
v = 1.0
|
| 568 |
+
elif coef_str == "-":
|
| 569 |
+
v = -1.0
|
| 570 |
+
else:
|
| 571 |
+
v = float(eval(coef_str))
|
| 572 |
+
coeffs[idx] += v
|
| 573 |
+
cons.append({'coeffs': coeffs, 'sense': sense, 'rhs': rhs})
|
| 574 |
+
return cons, sorted(list(set(free_vars)))
|
| 575 |
+
|
| 576 |
+
def expand_free_variables(nvars: int, c: List[float], constraints: List[Dict[str,Any]], free_vars: List[int]):
|
| 577 |
+
new_c = []
|
| 578 |
+
mapping = {}
|
| 579 |
+
for i in range(nvars):
|
| 580 |
+
if i in free_vars:
|
| 581 |
+
new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1)
|
| 582 |
+
new_c.append(-c[i]); mapping[len(new_c)-1] = (i, -1)
|
| 583 |
+
else:
|
| 584 |
+
new_c.append(c[i]); mapping[len(new_c)-1] = (i, +1)
|
| 585 |
+
new_constraints = []
|
| 586 |
+
for row in constraints:
|
| 587 |
+
coeffs = row['coeffs']
|
| 588 |
+
new_coeffs = []
|
| 589 |
+
for i in range(nvars):
|
| 590 |
+
if i in free_vars:
|
| 591 |
+
new_coeffs.append(coeffs[i])
|
| 592 |
+
new_coeffs.append(-coeffs[i])
|
| 593 |
+
else:
|
| 594 |
+
new_coeffs.append(coeffs[i])
|
| 595 |
+
new_constraints.append({'coeffs': new_coeffs, 'sense': row['sense'], 'rhs': row['rhs']})
|
| 596 |
+
return len(new_c), new_c, new_constraints, mapping
|
| 597 |
+
|
| 598 |
+
# ---------------- Tableau helpers ----------------
|
| 599 |
+
|
| 600 |
+
def snapshot_html(tableau: np.ndarray, basis: List[int]) -> str:
|
| 601 |
+
cols = tableau.shape[1]
|
| 602 |
+
html = '<table border="1" style="border-collapse:collapse;font-family:Arial; font-size:12px;">'
|
| 603 |
+
for i in range(tableau.shape[0]):
|
| 604 |
+
html += '<tr>'
|
| 605 |
+
for j in range(cols):
|
| 606 |
+
val = tableau[i, j]
|
| 607 |
+
html += f'<td style="padding:4px;">{val:.6g}</td>'
|
| 608 |
+
html += '</tr>'
|
| 609 |
+
html += '</table>'
|
| 610 |
+
return html
|
| 611 |
+
|
| 612 |
+
def primal_simplex_tableau(T: np.ndarray, basis: List[int], max_iters=1000) -> Tuple[np.ndarray, List[int], List[Dict[str,Any]]]:
|
| 613 |
+
m = T.shape[0] - 1
|
| 614 |
+
ncols = T.shape[1]
|
| 615 |
+
path = []
|
| 616 |
+
path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)})
|
| 617 |
+
|
| 618 |
+
it = 0
|
| 619 |
+
while it < max_iters:
|
| 620 |
+
it += 1
|
| 621 |
+
obj_row = T[-1, :-1]
|
| 622 |
+
entering_candidates = np.where(obj_row < -EPS)[0]
|
| 623 |
+
if entering_candidates.size == 0:
|
| 624 |
+
break
|
| 625 |
+
entering = int(entering_candidates[0])
|
| 626 |
+
ratios = np.full(m, np.inf)
|
| 627 |
+
for i in range(m):
|
| 628 |
+
a = T[i, entering]
|
| 629 |
+
if a > EPS:
|
| 630 |
+
ratios[i] = T[i, -1] / a
|
| 631 |
+
if np.all(np.isinf(ratios)):
|
| 632 |
+
raise Exception('Unbounded LP')
|
| 633 |
+
leaving = int(np.argmin(ratios))
|
| 634 |
+
piv = T[leaving, entering]
|
| 635 |
+
T[leaving, :] = T[leaving, :] / piv
|
| 636 |
+
for i in range(m+1):
|
| 637 |
+
if i == leaving: continue
|
| 638 |
+
T[i, :] = T[i, :] - T[i, entering] * T[leaving, :]
|
| 639 |
+
basis[leaving] = entering
|
| 640 |
+
path.append({'tableau': T.copy(), 'basis': basis.copy(), 'html': snapshot_html(T, basis)})
|
| 641 |
+
return T, basis, path
|
| 642 |
+
|
| 643 |
+
# ---------------- Two-Phase implementation (CORRIGIDA) ----------------
|
| 644 |
+
|
| 645 |
+
def build_tableau_two_phase(c: List[float], constraints: List[Dict[str,Any]], sense: str = 'max'):
|
| 646 |
+
obj_mult = 1.0
|
| 647 |
+
if sense == 'min':
|
| 648 |
+
obj_mult = -1.0
|
| 649 |
+
c_adj = [ci * obj_mult for ci in c]
|
| 650 |
+
|
| 651 |
+
n = len(c_adj)
|
| 652 |
+
m = len(constraints)
|
| 653 |
+
|
| 654 |
+
slacks = 0
|
| 655 |
+
artificials = 0
|
| 656 |
+
for row in constraints:
|
| 657 |
+
if row['sense'] == '<=':
|
| 658 |
+
slacks += 1
|
| 659 |
+
elif row['sense'] == '>=':
|
| 660 |
+
slacks += 1
|
| 661 |
+
artificials += 1
|
| 662 |
+
else:
|
| 663 |
+
artificials += 1
|
| 664 |
+
|
| 665 |
+
total_cols = n + slacks + artificials + 1
|
| 666 |
+
T = np.zeros((m + 1, total_cols))
|
| 667 |
+
|
| 668 |
+
slack_idx = n
|
| 669 |
+
artificial_idx = n + slacks
|
| 670 |
+
|
| 671 |
+
basis = []
|
| 672 |
+
art_positions = []
|
| 673 |
+
s_counter = 0
|
| 674 |
+
a_counter = 0
|
| 675 |
+
|
| 676 |
+
for i, row in enumerate(constraints):
|
| 677 |
+
coeffs = row['coeffs']
|
| 678 |
+
T[i, :n] = coeffs
|
| 679 |
+
if row['sense'] == '<=':
|
| 680 |
+
T[i, slack_idx + s_counter] = 1.0
|
| 681 |
+
basis.append(slack_idx + s_counter)
|
| 682 |
+
s_counter += 1
|
| 683 |
+
elif row['sense'] == '>=':
|
| 684 |
+
T[i, slack_idx + s_counter] = -1.0
|
| 685 |
+
T[i, artificial_idx + a_counter] = 1.0
|
| 686 |
+
basis.append(artificial_idx + a_counter)
|
| 687 |
+
art_positions.append(artificial_idx + a_counter)
|
| 688 |
+
s_counter += 1
|
| 689 |
+
a_counter += 1
|
| 690 |
+
else: # equality
|
| 691 |
+
T[i, artificial_idx + a_counter] = 1.0
|
| 692 |
+
basis.append(artificial_idx + a_counter)
|
| 693 |
+
art_positions.append(artificial_idx + a_counter)
|
| 694 |
+
a_counter += 1
|
| 695 |
+
T[i, -1] = row['rhs']
|
| 696 |
+
|
| 697 |
+
# Phase I objective: minimize sum of artificials.
|
| 698 |
+
# Convert to maximization for our tableau solver: maximize (-sum a_j)
|
| 699 |
+
# So c_phase1 (for maximization) = -1 for each artificial column.
|
| 700 |
+
c_phase1 = np.zeros(total_cols - 1)
|
| 701 |
+
for a in art_positions:
|
| 702 |
+
c_phase1[a] = -1.0
|
| 703 |
+
|
| 704 |
+
# In tableau we store -c in last row, so set T[-1, :-1] = -c_phase1
|
| 705 |
+
T[-1, :-1] = -c_phase1
|
| 706 |
+
|
| 707 |
+
# But because artificials are in basis, we must adjust objective row:
|
| 708 |
+
# T[-1, :] = -c + sum_{i in basis} c_Bi * row_i, where c_Bi = c_phase1[basis_i]
|
| 709 |
+
for i in range(m):
|
| 710 |
+
bi = basis[i]
|
| 711 |
+
cBi = c_phase1[bi] if bi < len(c_phase1) else 0.0
|
| 712 |
+
if abs(cBi) > EPS:
|
| 713 |
+
T[-1, :] += cBi * T[i, :]
|
| 714 |
+
|
| 715 |
+
return T, basis, (n, slacks, artificials), art_positions, c_adj
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
def run_two_phase(c, constraints, sense='max'):
|
| 719 |
+
|
| 720 |
+
#Implementação 100% por tableau — compatível com HuggingFace
|
| 721 |
+
#Phase I + Phase II completas (sem SciPy).
|
| 722 |
+
|
| 723 |
+
# ---------- PHASE I ----------
|
| 724 |
+
T0, basis0, (n_orig, n_slack, n_art), art_positions, c_adj = build_tableau_two_phase(
|
| 725 |
+
c, constraints, sense
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
try:
|
| 729 |
+
T1, basis1, path1 = primal_simplex_tableau(T0.copy(), basis0.copy())
|
| 730 |
+
except Exception as e:
|
| 731 |
+
return {
|
| 732 |
+
'status': 'phase1_failed',
|
| 733 |
+
'error': str(e),
|
| 734 |
+
'trace': traceback.format_exc()
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
phase1_obj = float(T1[-1, -1])
|
| 738 |
+
|
| 739 |
+
# se sum(a_j) != 0 → inviável
|
| 740 |
+
if abs(phase1_obj) > 1e-6:
|
| 741 |
+
return {
|
| 742 |
+
'status': 'infeasible',
|
| 743 |
+
'phase1_obj': phase1_obj,
|
| 744 |
+
'phase1_path': path1,
|
| 745 |
+
'tableau_phase1': T1
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
# ---------- REMOVER ARTIFICIAIS ----------
|
| 749 |
+
art_cols = set(art_positions)
|
| 750 |
+
old_ncols = T1.shape[1] - 1
|
| 751 |
+
keep_cols = [j for j in range(old_ncols) if j not in art_cols]
|
| 752 |
+
|
| 753 |
+
# construir tableau da Phase II (T2)
|
| 754 |
+
T2 = np.zeros((T1.shape[0], len(keep_cols) + 1))
|
| 755 |
+
for i, col in enumerate(keep_cols):
|
| 756 |
+
T2[:, i] = T1[:, col]
|
| 757 |
+
T2[:, -1] = T1[:, -1]
|
| 758 |
+
|
| 759 |
+
# nova base
|
| 760 |
+
basis2 = []
|
| 761 |
+
for bi in basis1:
|
| 762 |
+
if bi in art_cols:
|
| 763 |
+
basis2.append(None)
|
| 764 |
+
else:
|
| 765 |
+
basis2.append(keep_cols.index(bi))
|
| 766 |
+
|
| 767 |
+
# corrigir linhas onde a base ficou None
|
| 768 |
+
used = set([b for b in basis2 if b is not None])
|
| 769 |
+
m = T2.shape[0] - 1
|
| 770 |
+
|
| 771 |
+
for i in range(m):
|
| 772 |
+
if basis2[i] is None:
|
| 773 |
+
replaced = False
|
| 774 |
+
for j in range(T2.shape[1] - 1):
|
| 775 |
+
if j not in used and abs(T2[i, j]) > EPS:
|
| 776 |
+
piv = T2[i, j]
|
| 777 |
+
T2[i, :] = T2[i, :] / piv
|
| 778 |
+
for r in range(m+1):
|
| 779 |
+
if r != i:
|
| 780 |
+
T2[r, :] -= T2[r, j] * T2[i, :]
|
| 781 |
+
basis2[i] = j
|
| 782 |
+
used.add(j)
|
| 783 |
+
replaced = True
|
| 784 |
+
break
|
| 785 |
+
if not replaced:
|
| 786 |
+
basis2[i] = None
|
| 787 |
+
|
| 788 |
+
# ---------- PHASE II — definir objetivo original ----------
|
| 789 |
+
c_full = []
|
| 790 |
+
for col in keep_cols:
|
| 791 |
+
if col < len(c_adj):
|
| 792 |
+
c_full.append(c_adj[col])
|
| 793 |
+
else:
|
| 794 |
+
c_full.append(0.0)
|
| 795 |
+
|
| 796 |
+
c_full = np.array(c_full)
|
| 797 |
+
|
| 798 |
+
T2[-1, :-1] = -c_full
|
| 799 |
+
|
| 800 |
+
for i in range(m):
|
| 801 |
+
bi = basis2[i]
|
| 802 |
+
if bi is not None and bi < len(c_full):
|
| 803 |
+
coef = c_full[bi]
|
| 804 |
+
if abs(coef) > EPS:
|
| 805 |
+
T2[-1, :] += coef * T2[i, :]
|
| 806 |
+
|
| 807 |
+
# preencher bases ausentes
|
| 808 |
+
for i in range(m):
|
| 809 |
+
if basis2[i] is None:
|
| 810 |
+
for j in range(T2.shape[1]-1):
|
| 811 |
+
if j not in used:
|
| 812 |
+
basis2[i] = j
|
| 813 |
+
used.add(j)
|
| 814 |
+
break
|
| 815 |
+
|
| 816 |
+
# ---------- SIMPLEX PHASE II ----------
|
| 817 |
+
try:
|
| 818 |
+
T_final, basis_final, path2 = primal_simplex_tableau(T2.copy(), basis2.copy())
|
| 819 |
+
except Exception as e:
|
| 820 |
+
return {
|
| 821 |
+
'status': 'phase2_failed',
|
| 822 |
+
'error': str(e),
|
| 823 |
+
'phase1_path': path1,
|
| 824 |
+
'trace': traceback.format_exc()
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
# ---------- EXTRAI X*, REDUCED COSTS E DUAL (GERAL) ----------
|
| 828 |
+
x = [0.0] * n_orig
|
| 829 |
+
|
| 830 |
+
for i, bi in enumerate(basis_final):
|
| 831 |
+
if bi is not None:
|
| 832 |
+
oldcol = keep_cols[bi]
|
| 833 |
+
if oldcol < n_orig:
|
| 834 |
+
x[oldcol] = float(T_final[i, -1])
|
| 835 |
+
|
| 836 |
+
z = float(T_final[-1, -1])
|
| 837 |
+
|
| 838 |
+
# custos reduzidos apenas variáveis originais
|
| 839 |
+
reduced = []
|
| 840 |
+
for j in range(n_orig):
|
| 841 |
+
if j in keep_cols:
|
| 842 |
+
colpos = keep_cols.index(j)
|
| 843 |
+
z_j = -T_final[-1, colpos]
|
| 844 |
+
reduced.append(round(c_adj[j] - z_j, 8))
|
| 845 |
+
else:
|
| 846 |
+
reduced.append(0.0)
|
| 847 |
+
|
| 848 |
+
# Reconstruir matriz A (somente colunas originais) e b, c (originais)
|
| 849 |
+
A_orig = np.array([row['coeffs'] for row in constraints], dtype=float) # m x n_orig
|
| 850 |
+
b_vec = np.array([row['rhs'] for row in constraints], dtype=float)
|
| 851 |
+
cvec = np.array(c[:n_orig], dtype=float)
|
| 852 |
+
|
| 853 |
+
# Construir matriz M das colunas que permaneceram no tableau (T_final[:m, :-1])
|
| 854 |
+
M = T_final[:m, :-1].copy() # m x ncols_keep
|
| 855 |
+
|
| 856 |
+
# Basis matrix B (colunas básicas da fase II) — usar basis_final (índices em 0..ncols_keep-1)
|
| 857 |
+
# Garantir que não haja None; se houver, já tentamos preencher antes.
|
| 858 |
+
if any(bi is None for bi in basis_final):
|
| 859 |
+
# Em casos degenerados, preencher com pseudo-solução: y zeros
|
| 860 |
+
y_star = np.zeros(m)
|
| 861 |
+
else:
|
| 862 |
+
B = M[:, basis_final] # m x m
|
| 863 |
+
# montar c_B (custos das colunas básicas)
|
| 864 |
+
cB = np.zeros(m)
|
| 865 |
+
for i, bi in enumerate(basis_final):
|
| 866 |
+
if bi < len(c_full):
|
| 867 |
+
cB[i] = c_full[bi]
|
| 868 |
+
else:
|
| 869 |
+
cB[i] = 0.0
|
| 870 |
+
# y^T = cB^T * B^{-1}
|
| 871 |
+
try:
|
| 872 |
+
Binv = np.linalg.inv(B)
|
| 873 |
+
y_star = (cB @ Binv) # shape (m,)
|
| 874 |
+
except np.linalg.LinAlgError:
|
| 875 |
+
# fallback: tentar solução via least squares
|
| 876 |
+
try:
|
| 877 |
+
y_star, *_ = np.linalg.lstsq(B.T, cB, rcond=None)
|
| 878 |
+
except Exception:
|
| 879 |
+
y_star = np.zeros(m)
|
| 880 |
+
|
| 881 |
+
# dual objective b^T y
|
| 882 |
+
dual_obj = float(b_vec @ y_star)
|
| 883 |
+
|
| 884 |
+
# Definir folgas/violação das desigualdades do dual dependendo do sentido primal
|
| 885 |
+
# Se primal == 'max' -> dual é min b^T y s.t. A^T y >= c => slack = A^T y - c (>=0)
|
| 886 |
+
# Se primal == 'min' -> dual is max b^T y s.t. A^T y <= c => slack = c - A^T y (>=0)
|
| 887 |
+
if sense == 'max':
|
| 888 |
+
dual_slacks = (A_orig.T @ y_star) - cvec
|
| 889 |
+
else:
|
| 890 |
+
dual_slacks = cvec - (A_orig.T @ y_star)
|
| 891 |
+
|
| 892 |
+
# Preços-sombra (y) - ajustar sinal/convenção para exibição: mantemos y_star como calculado.
|
| 893 |
+
shadow = []
|
| 894 |
+
for i, row in enumerate(constraints):
|
| 895 |
+
shadow.append(round(float(y_star[i]), 8))
|
| 896 |
+
|
| 897 |
+
return {
|
| 898 |
+
'status': 'optimal',
|
| 899 |
+
'x': [round(v, 8) for v in x],
|
| 900 |
+
'obj': round(z, 8),
|
| 901 |
+
'y_dual': [round(float(v), 8) for v in y_star],
|
| 902 |
+
'dual_obj': round(dual_obj, 8),
|
| 903 |
+
'dual_slacks': [round(float(v), 8) for v in dual_slacks],
|
| 904 |
+
'A': A_orig.tolist(),
|
| 905 |
+
'b': b_vec.tolist(),
|
| 906 |
+
'c': cvec.tolist(),
|
| 907 |
+
'path_phase1': path1,
|
| 908 |
+
'path_phase2': path2,
|
| 909 |
+
'tableau_final': T_final,
|
| 910 |
+
'basis_final': basis_final,
|
| 911 |
+
'reduced_costs': reduced,
|
| 912 |
+
'shadow_prices': shadow
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
# ---------------- Helpers & PDF ----------------
|
| 917 |
+
|
| 918 |
+
def clean_vector(vec):
|
| 919 |
+
try:
|
| 920 |
+
return [float(v) for v in vec]
|
| 921 |
+
except:
|
| 922 |
+
return vec
|
| 923 |
+
|
| 924 |
+
|
| 925 |
+
# ---------------- Gradio handler ----------------
|
| 926 |
+
|
| 927 |
+
def run_algorithms(nvars_str, objective_str, cons_str, sense, mode):
|
| 928 |
+
try:
|
| 929 |
+
nvars = int(nvars_str)
|
| 930 |
+
if nvars <= 0:
|
| 931 |
+
return 'Erro: nvars deve ser inteiro positivo', '', '', '', ''
|
| 932 |
+
c = parse_coeffs(objective_str)
|
| 933 |
+
if len(c) != nvars:
|
| 934 |
+
return 'Erro: coeficientes do objetivo não correspondem a nvars', '', '', '', ''
|
| 935 |
+
constraints, free_vars = parse_constraints(cons_str, nvars)
|
| 936 |
+
if free_vars:
|
| 937 |
+
nvars, c, constraints, mapping = expand_free_variables(nvars, c, constraints, free_vars)
|
| 938 |
+
except Exception as e:
|
| 939 |
+
return f'Erro ao ler entrada: {e}', '', '', '', ''
|
| 940 |
+
|
| 941 |
+
res = run_two_phase(c, constraints, sense)
|
| 942 |
+
status = res.get('status')
|
| 943 |
+
# infeasible detected in Phase I
|
| 944 |
+
if status == 'infeasible':
|
| 945 |
+
return f"Problema inviável (Phase I obj = {res.get('phase1_obj')})", '', '', '', ''
|
| 946 |
+
|
| 947 |
+
# Phase I failed
|
| 948 |
+
if status == 'phase1_failed':
|
| 949 |
+
return f"Erro na Phase I: {res.get('error','(sem detalhe)')}", '', '', '', ''
|
| 950 |
+
|
| 951 |
+
if status == 'optimal':
|
| 952 |
+
x_primal = res['x']
|
| 953 |
+
z_primal = res['obj']
|
| 954 |
+
reduced = res.get('reduced_costs', [])
|
| 955 |
+
shadow = res.get('shadow_prices', [])
|
| 956 |
+
T_final = res.get('tableau_final', None)
|
| 957 |
+
path_primal = res.get('path_phase2', [])
|
| 958 |
+
path_phase1 = res.get('path_phase1', [])
|
| 959 |
+
y_dual = res.get('y_dual', [])
|
| 960 |
+
dual_obj = res.get('dual_obj', None)
|
| 961 |
+
dual_slacks = res.get('dual_slacks', [])
|
| 962 |
+
A = res.get('A', [])
|
| 963 |
+
b = res.get('b', [])
|
| 964 |
+
cvec = res.get('c', [])
|
| 965 |
+
else:
|
| 966 |
+
return f"Erro na resolução: status inesperado '{status}' - {res.get('error','')}", '', '', '', ''
|
| 967 |
+
|
| 968 |
+
steps_html_phase2 = ""
|
| 969 |
+
for idx, step in enumerate(path_primal):
|
| 970 |
+
steps_html_phase2 += f"<h4>Phase II — Passo {idx+1} — Base: {step.get('basis','?')}</h4>"
|
| 971 |
+
steps_html_phase2 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>"
|
| 972 |
+
|
| 973 |
+
steps_html_phase1 = ""
|
| 974 |
+
for idx, step in enumerate(path_phase1):
|
| 975 |
+
steps_html_phase1 += f"<h4>Phase I — Passo {idx+1} — Base: {step.get('basis','?')}</h4>"
|
| 976 |
+
steps_html_phase1 += snapshot_html(np.array(step['tableau']), step.get('basis', [])) + "<br/>"
|
| 977 |
+
|
| 978 |
+
df = pd.DataFrame({'Variável': [f'x{i+1}' for i in range(len(x_primal))], 'Valor': x_primal})
|
| 979 |
+
solution_html = df.to_html(index=False)
|
| 980 |
+
solution_html += f"<p><b>Valor ótimo (estimado) = {z_primal:.6g}</b></p>"
|
| 981 |
+
|
| 982 |
+
x_primal = clean_vector(x_primal); reduced = clean_vector(reduced); shadow = clean_vector(shadow)
|
| 983 |
+
z_primal = float(z_primal)
|
| 984 |
+
|
| 985 |
+
model_txt = f"Objective ({'min' if sense=='min' else 'max'}): {c}\nConstraints:\n"
|
| 986 |
+
for r in constraints:
|
| 987 |
+
model_txt += f" {r['coeffs']} {r['sense']} {r['rhs']}\n"
|
| 988 |
+
|
| 989 |
+
summary = ""
|
| 990 |
+
summary += f"Solução primal x* = {x_primal}\n"
|
| 991 |
+
summary += f"Z_primal (estimado) = {z_primal:.6g}\n\n"
|
| 992 |
+
|
| 993 |
+
summary += f"Solução dual y* = {y_dual}\n"
|
| 994 |
+
summary += f"Valor dual b^T y = {dual_obj}\n"
|
| 995 |
+
summary += f"Folgas/violação dual (A^T y - c) = {dual_slacks}\n\n"
|
| 996 |
+
|
| 997 |
+
summary += f"Preços-sombra (dual interpretado) = {shadow}\n"
|
| 998 |
+
summary += f"Custos reduzidos (vars originais) = {reduced}\n"
|
| 999 |
+
|
| 1000 |
+
return model_txt, solution_html, steps_html_phase2, steps_html_phase1, summary
|
| 1001 |
+
|
| 1002 |
+
# ---------------- Gradio UI ----------------
|
| 1003 |
+
|
| 1004 |
+
with gr.Blocks() as demo:
|
| 1005 |
+
gr.Markdown("# Simplex — Duas Fases (Phase I / Phase II) — Educational (Dual Geral)")
|
| 1006 |
+
with gr.Row():
|
| 1007 |
+
with gr.Column(scale=1):
|
| 1008 |
+
nvars = gr.Textbox(label='Número de variáveis (n)', value='2')
|
| 1009 |
+
objective = gr.Textbox(label='Coeficientes da função objetivo (ex: \"60 30\")', value='60 30')
|
| 1010 |
+
cons = gr.Textbox(label='Restrições (uma por linha). Ex.: 2x1 + 3x2 <= 300', lines=6,
|
| 1011 |
+
value='2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50')
|
| 1012 |
+
sense = gr.Radio(['max','min'], value='max', label='Tipo de objetivo')
|
| 1013 |
+
run = gr.Button('Executar Simplex (Duas Fases)')
|
| 1014 |
+
with gr.Column(scale=2):
|
| 1015 |
+
model_out = gr.Textbox(label='Função objetivo e restrições (modelo)', lines=6)
|
| 1016 |
+
solution_out = gr.HTML(label='Solução ótima (tabela)')
|
| 1017 |
+
steps_phase2_out = gr.HTML(label='Passos do Simplex (Phase II tableaus)')
|
| 1018 |
+
steps_phase1_out = gr.HTML(label='Passos do Simplex (Phase I tableaus)')
|
| 1019 |
+
summary_out = gr.Textbox(label='Resumo', lines=12)
|
| 1020 |
+
|
| 1021 |
+
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])
|
| 1022 |
+
|
| 1023 |
gr.Examples(examples=[["2","60 30","2x1 + 4x2 >= 40\n3x1 + 2x2 >= 50","max"]], inputs=[nvars, objective, cons, sense])
|
| 1024 |
|
| 1025 |
if __name__ == '__main__':
|