Spaces:
Sleeping
Sleeping
File size: 13,854 Bytes
52a2a94 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | """
Apogee Labs - Vibration Test Automation MVP
End-to-end demo:
Inputs (form + file uploads + engineer prompt)
-> [optional] Claude parses uploaded docs
-> Claude designs a structured fixture spec
-> CadQuery builds real STEP/STL/SVG geometry
-> Profile engine computes the random vibration PSD + Grms + notches
-> Claude writes design recommendations and the draft test report
-> Engineer edits + rates output (RLHF feedback capture)
Run:
pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-... # optional; mock without it
streamlit run app/main.py
"""
import os
import sys
import json
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import streamlit as st
import pandas as pd
from unified_retriever import UnifiedRetriever
from claude_client import generate_fixture_spec, generate_prose
from doc_parser import extract_with_claude
from feedback import save_feedback, feedback_stats
from cad_engine import (export_fixture, CADQUERY_AVAILABLE,
CADQUERY_IMPORT_ERROR)
from profile_engine import compute_profile, profile_as_table
from templates import (
FIXTURE_SPEC_SYSTEM, build_fixture_spec_prompt,
RECOMMENDATIONS_SYSTEM, build_recommendations_prompt,
REPORT_SYSTEM, build_report_prompt,
)
st.set_page_config(page_title="Apogee Labs - Vibration Test Automation",
page_icon="", layout="wide")
# On Streamlit Cloud, API keys arrive via st.secrets; copy them into the
# environment variables the rest of the code (and the SDKs) read.
# Locally, an already-set environment variable takes precedence.
try:
for _k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "VOYAGE_API_KEY"):
if not os.environ.get(_k) and _k in st.secrets:
os.environ[_k] = st.secrets[_k]
except Exception:
pass # st.secrets may not exist locally; that's fine
# --- session state ----------------------------------------------------------
for key in ("results", "extracted"):
st.session_state.setdefault(key, None)
# --- header -----------------------------------------------------------------
st.title(" Apogee Labs - Vibration Test Automation")
st.caption(
"MVP: fixture CAD design random vibration profile automated test report. "
"Grounded in public NASA/DoD standards. "
"**All output is a DRAFT requiring qualified test engineer review.**"
)
key_present = bool(os.environ.get("ANTHROPIC_API_KEY"))
c1, c2 = st.columns(2)
with c1:
if key_present:
st.success("Anthropic API key detected - live generation enabled.")
else:
st.warning("No ANTHROPIC_API_KEY - fixture/profile run live; "
"AI design text & doc parsing are mocked.")
with c2:
if CADQUERY_AVAILABLE:
st.success("CadQuery available - real CAD (STEP/STL) generation enabled.")
else:
st.error(f"CadQuery unavailable - CAD disabled. ({CADQUERY_IMPORT_ERROR}) "
"Run on Streamlit Cloud (Linux) where it installs cleanly.")
# ============================================================================
# 1. INPUTS
# ============================================================================
st.header("1. Inputs")
colA, colB = st.columns(2)
with colA:
article_name = st.text_input("Test article name", "3U CubeSat")
mass_kg = st.number_input("Test article mass (kg)", min_value=0.0,
value=4.0, step=0.5)
test_level = st.selectbox("Test level",
["qualification", "protoflight", "acceptance"])
with colB:
orbit = st.selectbox("Orbit / destination",
["LEO", "SSO", "MEO", "GEO", "Lunar", "Not specified"])
launch_vehicle = st.selectbox(
"Launch vehicle",
["Falcon 9", "Electron", "Vulcan", "New Glenn", "Not specified / TBD"])
resonances_str = st.text_input(
"Known resonant frequencies (Hz, comma-sep) - optional",
placeholder="e.g. 320, 610")
engineer_prompt = st.text_area(
"Test engineer request",
value="Design me a custom aluminum fixture to mount this article to the shaker.",
height=70)
uploads = st.file_uploader(
"Upload mission specs / launch vehicle guide / test article datasheet "
"(PDF or image) - optional",
type=["pdf", "png", "jpg", "jpeg"], accept_multiple_files=True)
# optional: parse uploads
if uploads and st.button(" Parse uploaded documents"):
if not key_present:
st.warning("Document parsing needs an API key. Skipping.")
else:
merged = {}
for uf in uploads:
with tempfile.NamedTemporaryFile(
delete=False, suffix=os.path.splitext(uf.name)[1]) as tmp:
tmp.write(uf.getbuffer())
tmp_path = tmp.name
with st.spinner(f"Parsing {uf.name}..."):
res = extract_with_claude(tmp_path)
if res["ok"]:
merged.update({k: v for k, v in res["data"].items() if v})
else:
st.error(f"{uf.name}: {res['error']}")
os.unlink(tmp_path)
st.session_state.extracted = merged or None
if merged:
st.success("Extracted parameters:")
st.json(merged)
# ============================================================================
# 2. GENERATE
# ============================================================================
st.header("2. Generate")
if st.button(" Generate Fixture + Profile + Report", type="primary"):
resonances = []
for tok in resonances_str.split(","):
tok = tok.strip()
if tok:
try:
resonances.append(float(tok))
except ValueError:
pass
inputs = {
"test_article_name": article_name,
"mass_kg": mass_kg,
"test_level": test_level,
"orbit": orbit,
"launch_vehicle": launch_vehicle,
"known_resonances_hz": resonances or "Not specified",
}
retriever = UnifiedRetriever()
chunks = retriever.all_chunks()
with st.spinner("Designing fixture..."):
spec_prompt = build_fixture_spec_prompt(
inputs, engineer_prompt, st.session_state.extracted)
spec_res = generate_fixture_spec(FIXTURE_SPEC_SYSTEM, spec_prompt, inputs)
spec = spec_res["spec"]
cad_paths = None
cad_error = None
if CADQUERY_AVAILABLE:
with st.spinner("Building CAD geometry (STEP / STL / preview)..."):
try:
out_dir = os.path.join(tempfile.gettempdir(), "apogee_cad")
cad_paths = export_fixture(spec, out_dir,
f"fixture_{article_name.replace(' ','_')}")
except Exception as e: # noqa: BLE001
cad_error = str(e)
with st.spinner("Computing vibration profile..."):
profile = compute_profile(test_level, mass_kg=mass_kg,
resonances_hz=resonances)
with st.spinner("Writing design recommendations..."):
rec_prompt = build_recommendations_prompt(inputs, spec.to_dict(), chunks)
rec = generate_prose(RECOMMENDATIONS_SYSTEM, rec_prompt,
label="recommendations")
with st.spinner("Generating draft test report..."):
profile_dict = {
"level": profile.level,
"duration_s_per_axis": profile.duration_s_per_axis,
"overall_grms": profile.overall_grms,
"mass_attenuation_db": profile.mass_attenuation_db,
"breakpoints": profile_as_table(profile),
"notches": profile.notches,
"notes": profile.notes,
}
rep_prompt = build_report_prompt(inputs, spec.to_dict(), profile_dict,
rec["text"], chunks)
report = generate_prose(REPORT_SYSTEM, rep_prompt, label="report",
max_tokens=3000)
st.session_state.results = {
"inputs": inputs, "spec": spec, "spec_mode": spec_res["mode"],
"cad_paths": cad_paths, "cad_error": cad_error,
"profile": profile, "profile_dict": profile_dict,
"recommendations": rec["text"], "report": report["text"],
}
# ============================================================================
# 3. RESULTS
# ============================================================================
res = st.session_state.results
if res:
st.header("3. Results")
tab1, tab2, tab3 = st.tabs(
[" Fixture Design", " Vibration Profile", " Test Report"])
# ---- Fixture --------------------------------------------------------
with tab1:
spec = res["spec"]
st.subheader("Custom Fixture Design")
if res["spec_mode"] != "live":
st.info(f"Fixture spec mode: {res['spec_mode']} "
"(set API key for AI-tailored design).")
lc, rc = st.columns([1, 1])
with lc:
if res["cad_paths"] and os.path.exists(res["cad_paths"]["svg"]):
with open(res["cad_paths"]["svg"], "r", encoding="utf-8") as f:
st.image(f.read(), caption="Fixture preview (isometric)")
elif res["cad_error"]:
st.error(f"CAD build error: {res['cad_error']}")
else:
st.info("CAD preview unavailable in this environment.")
with rc:
st.markdown(f"**Material:** {spec.material}")
st.markdown(f"**Est. mass:** {spec.estimated_mass_kg()} kg")
st.markdown(f"**Base:** {spec.base_length_mm}{spec.base_width_mm}"
f"{spec.base_thickness_mm} mm")
st.markdown(f"**Boss:** {spec.boss_length_mm}{spec.boss_width_mm}"
f"{spec.boss_height_mm} mm")
st.markdown(f"**Table bolts:** {spec.table_bolt_pattern.spacing_x_mm}"
f"{spec.table_bolt_pattern.spacing_y_mm} mm, "
f"{spec.table_bolt_pattern.hole_dia_mm} mm")
st.markdown(f"**Article bolts:** {spec.article_bolt_pattern.spacing_x_mm}"
f"{spec.article_bolt_pattern.spacing_y_mm} mm, "
f"{spec.article_bolt_pattern.hole_dia_mm} mm")
if spec.rationale:
st.markdown(f"**Design rationale:** {spec.rationale}")
if res["cad_paths"]:
d1, d2 = st.columns(2)
with d1:
with open(res["cad_paths"]["step"], "rb") as f:
st.download_button(" Download STEP", f.read(),
file_name="fixture.step")
with d2:
with open(res["cad_paths"]["stl"], "rb") as f:
st.download_button(" Download STL", f.read(),
file_name="fixture.stl")
st.divider()
st.markdown("### Design Recommendations")
st.markdown(res["recommendations"])
# ---- Profile --------------------------------------------------------
with tab2:
profile = res["profile"]
st.subheader("Random Vibration Test Profile")
m1, m2, m3 = st.columns(3)
m1.metric("Overall Grms", profile.overall_grms)
m2.metric("Duration/axis", f"{profile.duration_s_per_axis} s")
m3.metric("Mass atten.", f"{profile.mass_attenuation_db} dB")
df = pd.DataFrame(profile_as_table(profile))
# log-log PSD plot
chart_df = df.rename(columns={"Frequency (Hz)": "freq",
"ASD (g^2/Hz)": "asd"}).set_index("freq")
st.line_chart(chart_df)
st.caption("PSD breakpoints (GEVS generalized workmanship envelope, "
"adjusted for level and mass).")
st.dataframe(df, use_container_width=True)
if profile.notches:
st.markdown("**Suggested notches:**")
st.dataframe(pd.DataFrame(profile.notches), use_container_width=True)
for n in profile.notes:
st.markdown(f"- {n}")
# ---- Report ---------------------------------------------------------
with tab3:
st.subheader("Draft Test Report")
st.markdown(res["report"])
st.download_button(" Download report (Markdown)", res["report"],
file_name="vibration_test_report.md",
mime="text/markdown")
# ---- Feedback / RLHF loop ------------------------------------------
st.divider()
st.header("4. Test Engineer Feedback (improves the model)")
with st.form("feedback_form"):
usefulness = st.slider("How useful was this output?", 1, 5, 4)
was_edited = st.checkbox("I would edit/correct this before use")
what_improve = st.text_area("What should be improved?", height=70)
why_changes = st.text_area(
"If you'd change the design/profile, what and why?", height=70)
submitted = st.form_submit_button(" Submit feedback")
if submitted:
rec = save_feedback({
"inputs": res["inputs"],
"fixture_spec": res["spec"].to_dict(),
"profile": res["profile_dict"],
"usefulness": usefulness,
"was_edited": was_edited,
"what_improve": what_improve,
"why_changes": why_changes,
})
if rec["ok"]:
st.success(f"Feedback saved (id {rec['id'][:8]}). "
"This becomes a training example.")
else:
st.error(f"Save failed: {rec['error']}")
stats = feedback_stats()
if stats["count"]:
st.caption(f"Feedback collected: {stats['count']} "
f"avg usefulness {stats['avg_usefulness']} "
f"edited fraction {stats['edited_fraction']}")
|