kevinkyi commited on
Commit
91d2343
·
verified ·
1 Parent(s): ae5a71e

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. README.md +2 -2
  2. app.py +155 -39
README.md CHANGED
@@ -4,10 +4,10 @@ emoji: 🛩️
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: "5.47.2"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Interactive app that selects a transport wing from a candidate set, renders PNG + interactive 3D + STL,
13
  validates from an optional polar, and adds an LLM explanation.
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ Interactive app that selects a transport wing, renders PNG + interactive 3D + STL,
13
  validates from an optional polar, and adds an LLM explanation.
app.py CHANGED
@@ -564,52 +564,162 @@ def _topk_table_and_parallel(plans: List[Dict], probs: np.ndarray, k: int, objec
564
 
565
  # ---------------------- LLM: top-k grounded explanation (NEW) ----------------------
566
  # (Imports here to avoid import-time failures if transformers isn't available)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  try:
568
  from transformers import AutoTokenizer, AutoModelForCausalLM
569
  except Exception:
570
  AutoTokenizer, AutoModelForCausalLM = None, None
571
 
572
  _LOCAL_LLM_ID = os.getenv("LOCAL_LLM_ID", "Qwen/Qwen2.5-1.5B-Instruct")
573
- _tok = None
574
- _llm = None
575
- if AutoTokenizer is not None and AutoModelForCausalLM is not None:
 
 
 
 
 
 
 
576
  try:
577
  _tok = AutoTokenizer.from_pretrained(_LOCAL_LLM_ID)
 
 
578
  _llm = AutoModelForCausalLM.from_pretrained(
579
  _LOCAL_LLM_ID,
580
- device_map="auto",
581
- dtype=torch.float16 if torch.cuda.is_available() else torch.float32, # (not torch_dtype)
582
  )
583
- except Exception as _e:
 
 
 
584
  _tok, _llm = None, None
585
- print("[WARN] LLM not loaded:", _e)
 
 
586
 
587
  def _format_val(x):
588
- if isinstance(x, float):
589
- return f"{x:.3f}"
590
- return str(x)
591
 
592
- def _build_topk_context(best_row: Dict, rivals_df: pd.DataFrame, cols: List[str]) -> str:
593
  parts = []
594
  best_bits = [f"{c}={_format_val(best_row[c])}" for c in cols if c in best_row]
595
  parts.append("Best: " + ", ".join(best_bits) + f", score={_format_val(best_row.get('score', ''))}")
596
  if rivals_df is not None and not rivals_df.empty:
597
- r = min(4, len(rivals_df))
598
- for i in range(r):
599
  row = rivals_df.iloc[i].to_dict()
600
  bits = [f"{c}={_format_val(row[c])}" for c in cols if c in row]
601
  parts.append(f"Rival{ i+1 }: " + ", ".join(bits) + f", score={_format_val(row.get('score',''))}")
602
  return "\n".join(parts)
603
 
604
- def _llm_compare_and_explain(objective: str, best_row: Dict, rivals_df: pd.DataFrame) -> str:
605
- """
606
- Output exactly two short paragraphs:
607
- 1) 'Why this candidate:' — justify top-1 vs rivals using only feature names from context
608
- 2) 'Objective link:' — how those features help optimize the requested coefficient
609
- """
610
- if _tok is None or _llm is None:
611
- return ("Why this candidate: The top candidate aligns better with the objective using span/aspect ratio, taper, and washout than nearby rivals.\n"
612
- "Objective link: These features shape lift distribution and drag in a way consistent with the chosen coefficient optimization.")
613
  cols = ["span_m", "taper", "aspect_ratio", "mac_m", "washout_deg"]
614
  ctx = _build_topk_context(best_row, rivals_df, cols)
615
 
@@ -627,17 +737,23 @@ def _llm_compare_and_explain(objective: str, best_row: Dict, rivals_df: pd.DataF
627
  "Paragraph 2 must start with 'Objective link:' and use the features and objective to describe its real world performance/use case.\n"
628
  "Do not add bullet points or any numbers not present in the context."
629
  )
630
- messages = [
631
- {"role": "system", "content": sys_msg},
632
- {"role": "user", "content": user_msg},
633
- ]
 
 
 
 
634
  try:
635
  prompt = _tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
636
  except Exception:
637
- # Fallback for tokenizers without chat template
638
  prompt = sys_msg + "\n\n" + user_msg
639
 
640
- inputs = _tok(prompt, return_tensors="pt").to(_llm.device)
 
 
 
641
  with torch.no_grad():
642
  out_ids = _llm.generate(
643
  **inputs,
@@ -647,25 +763,25 @@ def _llm_compare_and_explain(objective: str, best_row: Dict, rivals_df: pd.DataF
647
  do_sample=True,
648
  repetition_penalty=1.05,
649
  )
650
- text = _tok.decode(out_ids[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
 
 
651
 
652
- # Keep just the two paragraphs starting with required prefixes
653
  lines = [l.strip() for l in text.splitlines() if l.strip()]
654
  joined = " ".join(lines)
655
  if "Why this candidate:" not in joined:
656
  joined = "Why this candidate: The top candidate presents a feature mix (aspect ratio, taper, washout) that better aligns with the objective than rivals. " + joined
657
  if "Objective link:" not in joined:
658
  joined += " Objective link: Those features influence lift distribution and drag in a way that supports the optimization."
659
- chunks = []
660
  for tag in ["Why this candidate:", "Objective link:"]:
661
- idx = joined.find(tag)
662
- if idx >= 0:
663
- end_candidates = [joined.find(t2, idx+1) for t2 in ["Why this candidate:", "Objective link:"] if joined.find(t2, idx+1) >= 0]
664
- end = min(end_candidates) if end_candidates else len(joined)
665
- chunks.append(joined[idx:end].strip())
666
- if len(chunks) >= 2:
667
- return chunks[0] + "\n" + chunks[1]
668
- return joined
669
 
670
  # --------------------- Quick validation (proxy) ---------------------
671
  def _interp_cl_cd(polar: Dict, alpha_deg: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
 
564
 
565
  # ---------------------- LLM: top-k grounded explanation (NEW) ----------------------
566
  # (Imports here to avoid import-time failures if transformers isn't available)
567
+ # try:
568
+ # from transformers import AutoTokenizer, AutoModelForCausalLM
569
+ # except Exception:
570
+ # AutoTokenizer, AutoModelForCausalLM = None, None
571
+
572
+ # _LOCAL_LLM_ID = os.getenv("LOCAL_LLM_ID", "Qwen/Qwen2.5-1.5B-Instruct")
573
+ # _tok = None
574
+ # _llm = None
575
+ # if AutoTokenizer is not None and AutoModelForCausalLM is not None:
576
+ # try:
577
+ # _tok = AutoTokenizer.from_pretrained(_LOCAL_LLM_ID)
578
+ # _llm = AutoModelForCausalLM.from_pretrained(
579
+ # _LOCAL_LLM_ID,
580
+ # device_map="auto",
581
+ # dtype=torch.float16 if torch.cuda.is_available() else torch.float32, # (not torch_dtype)
582
+ # )
583
+ # except Exception as _e:
584
+ # _tok, _llm = None, None
585
+ # print("[WARN] LLM not loaded:", _e)
586
+
587
+ # def _format_val(x):
588
+ # if isinstance(x, float):
589
+ # return f"{x:.3f}"
590
+ # return str(x)
591
+
592
+ # def _build_topk_context(best_row: Dict, rivals_df: pd.DataFrame, cols: List[str]) -> str:
593
+ # parts = []
594
+ # best_bits = [f"{c}={_format_val(best_row[c])}" for c in cols if c in best_row]
595
+ # parts.append("Best: " + ", ".join(best_bits) + f", score={_format_val(best_row.get('score', ''))}")
596
+ # if rivals_df is not None and not rivals_df.empty:
597
+ # r = min(4, len(rivals_df))
598
+ # for i in range(r):
599
+ # row = rivals_df.iloc[i].to_dict()
600
+ # bits = [f"{c}={_format_val(row[c])}" for c in cols if c in row]
601
+ # parts.append(f"Rival{ i+1 }: " + ", ".join(bits) + f", score={_format_val(row.get('score',''))}")
602
+ # return "\n".join(parts)
603
+
604
+ # def _llm_compare_and_explain(objective: str, best_row: Dict, rivals_df: pd.DataFrame) -> str:
605
+ # """
606
+ # Output exactly two short paragraphs:
607
+ # 1) 'Why this candidate:' — justify top-1 vs rivals using only feature names from context
608
+ # 2) 'Objective link:' — how those features help optimize the requested coefficient
609
+ # """
610
+ # if _tok is None or _llm is None:
611
+ # return ("Why this candidate: The top candidate aligns better with the objective using span/aspect ratio, taper, and washout than nearby rivals.\n"
612
+ # "Objective link: These features shape lift distribution and drag in a way consistent with the chosen coefficient optimization.")
613
+ # cols = ["span_m", "taper", "aspect_ratio", "mac_m", "washout_deg"]
614
+ # ctx = _build_topk_context(best_row, rivals_df, cols)
615
+
616
+ # sys_msg = (
617
+ # "You are an aerospace engineering assistant. "
618
+ # "Use ONLY the provided context lines (feature names) to justify the selection. "
619
+ # "Do not invent numbers; if referencing a quantity, use its feature name only."
620
+ # )
621
+ # user_msg = (
622
+ # f"Objective: {objective}\n"
623
+ # "Context lines (these are the only values you may rely on):\n"
624
+ # f"{ctx}\n\n"
625
+ # "Write exactly two short paragraphs (1–2 sentences each):\n"
626
+ # "Paragraph 1 must start with 'Why this candidate:' and compare the best to rivals using feature names.\n"
627
+ # "Paragraph 2 must start with 'Objective link:' and use the features and objective to describe its real world performance/use case.\n"
628
+ # "Do not add bullet points or any numbers not present in the context."
629
+ # )
630
+ # messages = [
631
+ # {"role": "system", "content": sys_msg},
632
+ # {"role": "user", "content": user_msg},
633
+ # ]
634
+ # try:
635
+ # prompt = _tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
636
+ # except Exception:
637
+ # # Fallback for tokenizers without chat template
638
+ # prompt = sys_msg + "\n\n" + user_msg
639
+
640
+ # inputs = _tok(prompt, return_tensors="pt").to(_llm.device)
641
+ # with torch.no_grad():
642
+ # out_ids = _llm.generate(
643
+ # **inputs,
644
+ # max_new_tokens=220,
645
+ # temperature=0.2,
646
+ # top_p=0.9,
647
+ # do_sample=True,
648
+ # repetition_penalty=1.05,
649
+ # )
650
+ # text = _tok.decode(out_ids[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
651
+
652
+ # # Keep just the two paragraphs starting with required prefixes
653
+ # lines = [l.strip() for l in text.splitlines() if l.strip()]
654
+ # joined = " ".join(lines)
655
+ # if "Why this candidate:" not in joined:
656
+ # joined = "Why this candidate: The top candidate presents a feature mix (aspect ratio, taper, washout) that better aligns with the objective than rivals. " + joined
657
+ # if "Objective link:" not in joined:
658
+ # joined += " Objective link: Those features influence lift distribution and drag in a way that supports the optimization."
659
+ # chunks = []
660
+ # for tag in ["Why this candidate:", "Objective link:"]:
661
+ # idx = joined.find(tag)
662
+ # if idx >= 0:
663
+ # end_candidates = [joined.find(t2, idx+1) for t2 in ["Why this candidate:", "Objective link:"] if joined.find(t2, idx+1) >= 0]
664
+ # end = min(end_candidates) if end_candidates else len(joined)
665
+ # chunks.append(joined[idx:end].strip())
666
+ # if len(chunks) >= 2:
667
+ # return chunks[0] + "\n" + chunks[1]
668
+ # return joined
669
+
670
+ # ---------------------- LLM: top-k grounded explanation ----------------------
671
+ import os
672
+ import torch
673
+
674
  try:
675
  from transformers import AutoTokenizer, AutoModelForCausalLM
676
  except Exception:
677
  AutoTokenizer, AutoModelForCausalLM = None, None
678
 
679
  _LOCAL_LLM_ID = os.getenv("LOCAL_LLM_ID", "Qwen/Qwen2.5-1.5B-Instruct")
680
+ _LLM_LOAD_ERR = None
681
+ _tok, _llm = None, None
682
+
683
+ def _try_load_llm():
684
+ global _tok, _llm, _LLM_LOAD_ERR
685
+ if (_tok is not None) and (_llm is not None):
686
+ return True
687
+ if AutoTokenizer is None or AutoModelForCausalLM is None:
688
+ _LLM_LOAD_ERR = "transformers not available"
689
+ return False
690
  try:
691
  _tok = AutoTokenizer.from_pretrained(_LOCAL_LLM_ID)
692
+ # On Spaces CPU: be explicit — CPU + float32
693
+ torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
694
  _llm = AutoModelForCausalLM.from_pretrained(
695
  _LOCAL_LLM_ID,
696
+ torch_dtype=torch_dtype,
 
697
  )
698
+ if not torch.cuda.is_available():
699
+ _llm = _llm.to("cpu")
700
+ return True
701
+ except Exception as e:
702
  _tok, _llm = None, None
703
+ _LLM_LOAD_ERR = f"{type(e).__name__}: {e}"
704
+ print("[WARN] LLM not loaded:", _LLM_LOAD_ERR)
705
+ return False
706
 
707
  def _format_val(x):
708
+ return f"{x:.3f}" if isinstance(x, float) else str(x)
 
 
709
 
710
+ def _build_topk_context(best_row, rivals_df, cols):
711
  parts = []
712
  best_bits = [f"{c}={_format_val(best_row[c])}" for c in cols if c in best_row]
713
  parts.append("Best: " + ", ".join(best_bits) + f", score={_format_val(best_row.get('score', ''))}")
714
  if rivals_df is not None and not rivals_df.empty:
715
+ for i in range(min(4, len(rivals_df))):
 
716
  row = rivals_df.iloc[i].to_dict()
717
  bits = [f"{c}={_format_val(row[c])}" for c in cols if c in row]
718
  parts.append(f"Rival{ i+1 }: " + ", ".join(bits) + f", score={_format_val(row.get('score',''))}")
719
  return "\n".join(parts)
720
 
721
+ def _llm_compare_and_explain(objective: str, best_row: dict, rivals_df: "pd.DataFrame") -> str:
722
+ ok = _try_load_llm()
 
 
 
 
 
 
 
723
  cols = ["span_m", "taper", "aspect_ratio", "mac_m", "washout_deg"]
724
  ctx = _build_topk_context(best_row, rivals_df, cols)
725
 
 
737
  "Paragraph 2 must start with 'Objective link:' and use the features and objective to describe its real world performance/use case.\n"
738
  "Do not add bullet points or any numbers not present in the context."
739
  )
740
+
741
+ if not ok:
742
+ # Show the real reason in the UI so you can diagnose Spaces vs Colab.
743
+ reason = _LLM_LOAD_ERR or "unknown"
744
+ return ("Why this candidate: The top candidate aligns better with the objective using span/aspect ratio, taper, and washout than nearby rivals.\n"
745
+ f"Objective link: (LLM fallback) {reason}. Features still indicate a lift/drag balance consistent with the selected objective.")
746
+
747
+ messages = [{"role": "system", "content": sys_msg}, {"role": "user", "content": user_msg}]
748
  try:
749
  prompt = _tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
750
  except Exception:
 
751
  prompt = sys_msg + "\n\n" + user_msg
752
 
753
+ inputs = _tok(prompt, return_tensors="pt")
754
+ if torch.cuda.is_available():
755
+ inputs = inputs.to(_llm.device)
756
+
757
  with torch.no_grad():
758
  out_ids = _llm.generate(
759
  **inputs,
 
763
  do_sample=True,
764
  repetition_penalty=1.05,
765
  )
766
+ # slice off the prompt
767
+ start = inputs["input_ids"].shape[1]
768
+ text = _tok.decode(out_ids[0, start:], skip_special_tokens=True).strip()
769
 
770
+ # Normalize to the two required paragraphs:
771
  lines = [l.strip() for l in text.splitlines() if l.strip()]
772
  joined = " ".join(lines)
773
  if "Why this candidate:" not in joined:
774
  joined = "Why this candidate: The top candidate presents a feature mix (aspect ratio, taper, washout) that better aligns with the objective than rivals. " + joined
775
  if "Objective link:" not in joined:
776
  joined += " Objective link: Those features influence lift distribution and drag in a way that supports the optimization."
777
+ parts = []
778
  for tag in ["Why this candidate:", "Objective link:"]:
779
+ i = joined.find(tag)
780
+ if i >= 0:
781
+ j = min([x for x in (joined.find("Why this candidate:", i+1), joined.find("Objective link:", i+1)) if x != -1] + [len(joined)])
782
+ parts.append(joined[i:j].strip())
783
+ return "\n".join(parts[:2]) if parts else joined
784
+
 
 
785
 
786
  # --------------------- Quick validation (proxy) ---------------------
787
  def _interp_cl_cd(polar: Dict, alpha_deg: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: