beam-explainer / app.py
ysakhale's picture
Update app.py
3ca1fe0 verified
import os, shutil
for cache_dir in ["/root/.cache/huggingface", "/root/.cache/torch", "/root/.cache/autogluon"]:
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir, ignore_errors=True)
import math
import gradio
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
# Model for explanations (small, CPU-friendly)
MODEL_ID = "HuggingFaceTB/SmolLM2-135M-Instruct"
# Load model and tokenizer once
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID)
pipe = pipeline(task="text-generation", model=model, tokenizer=tokenizer)
def beam_calculation_engine(L_m: float, a_m: float, P_N: float, E_GPa: float, Sy_MPa: float) -> dict:
if any(val <= 0 for val in [L_m, a_m, P_N, E_GPa, Sy_MPa]):
raise ValueError("All input parameters must be positive values")
E_Pa = E_GPa * 1e9
Sy_Pa = Sy_MPa * 1e6
I = a_m**4 / 12.0
c = a_m / 2.0
M_max = P_N * L_m / 4.0
sigma_max_Pa = M_max * c / I
sigma_max_MPa = sigma_max_Pa / 1e6
delta_max_m = (P_N * L_m**3) / (48.0 * E_Pa * I)
delta_max_mm = delta_max_m * 1000
delta_allowable_m = L_m / 360.0
delta_allowable_mm = delta_allowable_m * 1000
stress_ratio = sigma_max_Pa / Sy_Pa
fos_yield = 1.0 / stress_ratio if stress_ratio > 0 else float("inf")
deflection_ratio = delta_max_m / delta_allowable_m
fos_deflection = 1.0 / deflection_ratio if deflection_ratio > 0 else float("inf")
passes_strength = sigma_max_Pa <= Sy_Pa
passes_serviceability = delta_max_m <= delta_allowable_m
overall_safe = passes_strength and passes_serviceability
calculation_summary = {
"beam_properties": {
"length_m": L_m,
"cross_section_side_m": a_m,
"load_N": P_N,
"elastic_modulus_GPa": E_GPa,
"yield_strength_MPa": Sy_MPa
},
"calculated_values": {
"maximum_stress_MPa": round(sigma_max_MPa, 3),
"maximum_deflection_mm": round(delta_max_mm, 3),
"allowable_deflection_mm": round(delta_allowable_mm, 3),
"factor_of_safety_yield": round(fos_yield, 3),
"factor_of_safety_deflection": round(fos_deflection, 3)
},
"engineering_verdict": {
"strength_check": "PASS" if passes_strength else "FAIL",
"serviceability_check": "PASS" if passes_serviceability else "FAIL",
"overall_safety": "SAFE" if overall_safe else "UNSAFE",
"strength_message": f"Stress ({sigma_max_MPa:.1f} MPa) {'≤' if passes_strength else '>'} Yield ({Sy_MPa} MPa)",
"deflection_message": f"Deflection ({delta_max_mm:.1f} mm) {'≤' if passes_serviceability else '>'} Allowable ({delta_allowable_mm:.1f} mm)"
},
"human_readable_summary": (
f"Beam Analysis Results:\n"
f"- Maximum bending stress: {sigma_max_MPa:.1f} MPa\n"
f"- Maximum deflection: {delta_max_mm:.1f} mm\n"
f"- Allowable deflection: {delta_allowable_mm:.1f} mm\n"
f"- Factor of safety (yield): {fos_yield:.2f}\n"
f"- Factor of safety (deflection): {fos_deflection:.2f}\n"
f"- Strength check: {'PASS' if passes_strength else 'FAIL'}\n"
f"- Serviceability check: {'PASS' if passes_serviceability else 'FAIL'}\n"
f"- Overall verdict: {'SAFE' if overall_safe else 'UNSAFE'}"
)
}
return calculation_summary
def format_llm_prompt(system_prompt: str, user_prompt: str) -> str:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
return f"System: {system_prompt}\n\nUser: {user_prompt}\n\nAssistant:"
def generate_explanation(structured_results: dict) -> str:
props = structured_results["beam_properties"]
calc = structured_results["calculated_values"]
verdict = structured_results["engineering_verdict"]
system_prompt = (
"You are an engineering educator explaining structural analysis results. "
"Be accurate, concise, and ground the explanation only in provided numbers. "
"No new assumptions or invented figures."
)
user_prompt = (
f"Explain these beam results in simple terms:\n"
f"Beam length: {props['length_m']} m, square side: {props['cross_section_side_m']*1000:.1f} mm, "
f"load: {props['load_N']/1000:.1f} kN. Material: E={props['elastic_modulus_GPa']} GPa, Sy={props['yield_strength_MPa']} MPa.\n"
f"Stress={calc['maximum_stress_MPa']} MPa, deflection={calc['maximum_deflection_mm']} mm, "
f"allowable deflection={calc['allowable_deflection_mm']} mm. "
f"FoS(yield)={calc['factor_of_safety_yield']}, FoS(deflection)={calc['factor_of_safety_deflection']}.\n"
f"Verdict: Strength={verdict['strength_check']}, Serviceability={verdict['serviceability_check']}, Overall={verdict['overall_safety']}."
)
prompt = format_llm_prompt(system_prompt, user_prompt)
try:
# Deterministic generation: greedy decoding (no sampling)
out = pipe(prompt, max_new_tokens=150, do_sample=False, return_full_text=False)
explanation = out[0]["generated_text"].strip()
except Exception as e:
explanation = f"Explanation unavailable: {e}. Raw results:\n{structured_results['human_readable_summary']}"
return explanation
def run_beam_analysis(L_m, a_m, P_kN, E_GPa, Sy_MPa):
try:
L_m = float(L_m)
a_m = float(a_m)
P_kN = float(P_kN)
E_GPa = float(E_GPa)
Sy_MPa = float(Sy_MPa)
if not (0.5 <= L_m <= 10.0):
return None, None, "Error: Beam length must be between 0.5 and 10.0 meters"
if not (0.01 <= a_m <= 0.5):
return None, None, "Error: Cross-section side must be between 0.01 and 0.5 meters"
if not (0.1 <= P_kN <= 100.0):
return None, None, "Error: Load must be between 0.1 and 100.0 kN"
if not (50.0 <= E_GPa <= 300.0):
return None, None, "Error: Elastic modulus must be between 50 and 300 GPa"
if not (100.0 <= Sy_MPa <= 1000.0):
return None, None, "Error: Yield strength must be between 100 and 1000 MPa"
P_N = P_kN * 1000.0
results = beam_calculation_engine(L_m, a_m, P_N, E_GPa, Sy_MPa)
results_df = pd.DataFrame([
{"Parameter": "Maximum Stress", "Value": f"{results['calculated_values']['maximum_stress_MPa']} MPa", "Limit": f"{Sy_MPa} MPa", "Status": results['engineering_verdict']['strength_check']},
{"Parameter": "Factor of Safety (Yield)", "Value": f"{results['calculated_values']['factor_of_safety_yield']}", "Limit": "> 1.0", "Status": "PASS" if results['calculated_values']['factor_of_safety_yield'] > 1.0 else "FAIL"},
{"Parameter": "Maximum Deflection", "Value": f"{results['calculated_values']['maximum_deflection_mm']} mm", "Limit": f"{results['calculated_values']['allowable_deflection_mm']} mm", "Status": results['engineering_verdict']['serviceability_check']},
{"Parameter": "Factor of Safety (Deflection)", "Value": f"{results['calculated_values']['factor_of_safety_deflection']}", "Limit": "> 1.0", "Status": "PASS" if results['calculated_values']['factor_of_safety_deflection'] > 1.0 else "FAIL"},
])
explanation = generate_explanation(results)
return results_df, results['human_readable_summary'], explanation
except Exception as e:
return None, None, f"Calculation error: {e}"
with gradio.Blocks(title="Beam Analysis Calculator") as demo:
gradio.Markdown("# Beam Analysis Calculator with Natural Language Explanations")
gradio.Markdown("Deterministic simply-supported beam analysis with a central point load. See the numbers and the plain-language explanation.")
with gradio.Row():
with gradio.Column(scale=1):
gradio.Markdown("## Inputs")
with gradio.Row():
L_m = gradio.Number(value=2.0, label="Beam Length (m)", minimum=0.5, maximum=10.0, info="Support-to-support length")
a_m = gradio.Number(value=0.05, label="Square Side (m)", minimum=0.01, maximum=0.5, info="Side length of square cross-section")
with gradio.Row():
P_kN = gradio.Number(value=5.0, label="Central Point Load (kN)", minimum=0.1, maximum=100.0, info="Load applied at midspan")
with gradio.Row():
E_GPa = gradio.Number(value=200.0, label="Elastic Modulus (GPa)", minimum=50.0, maximum=300.0, info="Young's modulus of material")
Sy_MPa = gradio.Number(value=250.0, label="Yield Strength (MPa)", minimum=100.0, maximum=1000.0, info="Material yield strength")
calculate_btn = gradio.Button("Run Analysis")
gradio.Markdown("### Example Cases")
gradio.Examples(
examples=[
[2.0, 0.05, 5.0, 200.0, 250.0],
[3.0, 0.08, 10.0, 70.0, 200.0],
[1.5, 0.03, 2.0, 210.0, 350.0],
],
inputs=[L_m, a_m, P_kN, E_GPa, Sy_MPa],
label="Click to load parameters"
)
with gradio.Column(scale=1):
gradio.Markdown("## Results")
results_table = gradio.Dataframe(headers=["Parameter", "Value", "Limit", "Status"], interactive=False, label="Calculation Results")
detailed_output = gradio.Textbox(label="Detailed Calculation Summary", interactive=False, lines=6)
explanation_output = gradio.Textbox(label="Natural Language Explanation", interactive=False, lines=6)
calculate_btn.click(fn=run_beam_analysis, inputs=[L_m, a_m, P_kN, E_GPa, Sy_MPa], outputs=[results_table, detailed_output, explanation_output])
if __name__ == "__main__":
demo.launch()