Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
README.md
CHANGED
|
@@ -4,10 +4,10 @@ emoji: 🛩️
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
Interactive app that selects a transport wing
|
| 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 |
-
|
| 574 |
-
_llm = None
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
try:
|
| 577 |
_tok = AutoTokenizer.from_pretrained(_LOCAL_LLM_ID)
|
|
|
|
|
|
|
| 578 |
_llm = AutoModelForCausalLM.from_pretrained(
|
| 579 |
_LOCAL_LLM_ID,
|
| 580 |
-
|
| 581 |
-
dtype=torch.float16 if torch.cuda.is_available() else torch.float32, # (not torch_dtype)
|
| 582 |
)
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
| 584 |
_tok, _llm = None, None
|
| 585 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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:
|
| 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 |
-
|
| 631 |
-
|
| 632 |
-
|
| 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")
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 651 |
|
| 652 |
-
#
|
| 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 |
-
|
| 660 |
for tag in ["Why this candidate:", "Objective link:"]:
|
| 661 |
-
|
| 662 |
-
if
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 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]:
|