pedrobritto-123 commited on
Commit
425b0ce
·
verified ·
1 Parent(s): 2369b26

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +529 -1
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__':