CoolFace
Apppublic

cdshelat/MorphAI

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
1"""2MorphAI — FastAPI backend3Wraps the existing optimizer / materials / geometry pipeline and serves the React frontend.4"""5from __future__ import annotations6 7import json8import uuid9from pathlib import Path10 11import numpy as np12from fastapi import FastAPI, HTTPException13from fastapi.responses import FileResponse, Response14from fastapi.staticfiles import StaticFiles15from pydantic import BaseModel16 17from materials import MATERIALS18from optimizer import simp_core, build_filter, build_load_cases19from geometry import to_stl_bytes20from utils import estimate_print21 22# ── Storage paths ──────────────────────────────────────────────────────────────23MORPHAI_DIR = Path.home() / ".morphai"24HISTORY_FILE = MORPHAI_DIR / "history.json"25STL_DIR = MORPHAI_DIR / "stls"26 27 28def _ensure_dirs():29    MORPHAI_DIR.mkdir(parents=True, exist_ok=True)30    STL_DIR.mkdir(parents=True, exist_ok=True)31 32 33def _load_history() -> dict:34    _ensure_dirs()35    if HISTORY_FILE.exists():36        try:37            return json.loads(HISTORY_FILE.read_text(encoding="utf-8"))38        except Exception:39            pass40    return {"runs": []}41 42 43def _save_history(data: dict):44    _ensure_dirs()45    HISTORY_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")46 47 48# ── Material name mapping (frontend short → backend full key) ──────────────────49_MAT_MAP = {50    "PLA":            "PLA (Bioplastic)",51    "PETG":           "PETG (Engineering Plastic)",52    "ABS":            "ABS (Acrylonitrile Butadiene Styrene)",53    "Nylon":          "Nylon PA12",54    "CF-PETG":        "Carbon Fiber PETG (Composite)",55    "Aluminum (ref)": "Aluminum 6061 (Reference)",56    "Titanium (ref)": "Titanium Ti-6Al-4V (Reference)",57}58 59# ── Face name mapping (frontend short → backend full) ─────────────────────────60_FACE_MAP = {61    "Left":   "Left (X=0)",62    "Right":  "Right (X=W)",63    "Top":    "Top (Y=H)",64    "Bottom": "Bottom (Y=0)",65    "Front":  "Front (Z=0)",66    "Back":   "Back (Z=D)",67}68 69# ── Force direction unit vectors (frontend dir → (fx, fy)) ────────────────────70_DIR_UNIT = {71    "-X": (-1.0,  0.0),72    "+X": ( 1.0,  0.0),73    "-Y": ( 0.0, -1.0),74    "+Y": ( 0.0,  1.0),75    "-Z": ( 0.0,  0.0),76    "+Z": ( 0.0,  0.0),77}78 79 80# ── Metrics helpers ────────────────────────────────────────────────────────────81 82def _compute_bimodality(xPhys: np.ndarray) -> float:83    flat = xPhys.flatten()84    return float(((flat < 0.2) | (flat > 0.8)).sum() / len(flat))85 86 87def _check_load_path(xPhys: np.ndarray, fixed_face: str, load_face: str) -> bool:88    """BFS flood-fill: is there a solid-element path between the two faces?"""89    nely, nelx = xPhys.shape90    solid = xPhys > 0.4591 92    def _face_elements(face: str) -> set:93        s: set = set()94        if "Left" in face:95            for iy in range(nely):96                if solid[iy, 0]: s.add((iy, 0))97        elif "Right" in face:98            for iy in range(nely):99                if solid[iy, nelx - 1]: s.add((iy, nelx - 1))100        elif "Bottom" in face:101            for ix in range(nelx):102                if solid[0, ix]: s.add((0, ix))103        elif "Top" in face:104            for ix in range(nelx):105                if solid[nely - 1, ix]: s.add((nely - 1, ix))106        return s107 108    seeds = _face_elements(fixed_face)109    targets = _face_elements(load_face)110 111    if not seeds or not targets:112        return False113    if seeds & targets:114        return True115 116    visited = set(seeds)117    frontier = list(seeds)118    while frontier:119        iy, ix = frontier.pop()120        if (iy, ix) in targets:121            return True122        for dy, dx in ((-1, 0), (1, 0), (0, -1), (0, 1)):123            ny, nx = iy + dy, ix + dx124            if (0 <= ny < nely and 0 <= nx < nelx125                    and solid[ny, nx] and (ny, nx) not in visited):126                visited.add((ny, nx))127                frontier.append((ny, nx))128    return False129 130 131def _check_non_convergent(history: list[float], threshold: float = 8) -> bool:132    """Return True if compliance stopped improving for the last `threshold` iters."""133    if len(history) < threshold + 2:134        return False135    recent = history[-int(threshold):]136    span = max(recent) - min(recent)137    reference = abs(history[0]) if history[0] else 1.0138    return span < reference * 0.001139 140 141# ── Fidelity presets ───────────────────────────────────────────────────────────142_FIDELITY = {143    "quick":    {"nelx_base": 14, "max_iter": 40},144    "standard": {"nelx_base": 22, "max_iter": 60},145    "detail":   {"nelx_base": 34, "max_iter": 80},146}147 148 149# ── Request / response models ──────────────────────────────────────────────────150 151class OptimizeRequest(BaseModel):152    material:  str   = "PLA"153    w:         float = 100.0   # mm154    h:         float = 60.0155    d:         float = 30.0156    fixedFace: str   = "Left"157    loadFace:  str   = "Right"158    forceDir:  str   = "-Y"159    force:     float = 150.0   # N160    vf:        float = 0.40161    sf:        float = 2.0162    fidelity:  str   = "standard"163    infill:    str   = "Gyroid"164 165 166# ── App ────────────────────────────────────────────────────────────────────────167app = FastAPI(title="MorphAI API", version="1.0")168 169 170@app.get("/api/materials")171def get_materials():172    """Return material catalogue using frontend short names."""173    result = {}174    for short, full in _MAT_MAP.items():175        m = MATERIALS.get(full)176        if m is None:177            continue178        result[short] = {179            "E":         m["E_gpa"],180            "rho":       m["rho_gcc"],181            "yield":     m["yield_strength_mpa"],182            "color":     m["color"],183            "printable": m["printable"],184            "temp":      m["print_temp"],185            "cost":      round(m["cost_per_kg"] / 1000, 4),  # per gram186            "desc":      m.get("notes", ""),187        }188    return result189 190 191@app.post("/api/optimize")192def run_optimize(req: OptimizeRequest):193    # ── Resolve material ───────────────────────────────────────────────────────194    mat_full = _MAT_MAP.get(req.material, "PLA (Bioplastic)")195    mat = MATERIALS.get(mat_full)196    if mat is None:197        raise HTTPException(400, f"Unknown material: {req.material}")198 199    nu = mat["nu"]200 201    # ── Mesh resolution from fidelity ─────────────────────────────────────────202    fid = _FIDELITY.get(req.fidelity, _FIDELITY["standard"])203    nelx = fid["nelx_base"]204    nely = max(6, round(nelx * req.h / req.w))205    max_iter = fid["max_iter"]206 207    # ── Build filter ───────────────────────────────────────────────────────────208    H, Hs = build_filter(nelx, nely, rmin=1.5)209 210    # ── Build load/support specs ───────────────────────────────────────────────211    fixed_face_full = _FACE_MAP.get(req.fixedFace, "Left (X=0)")212    load_face_full  = _FACE_MAP.get(req.loadFace,  "Right (X=W)")213    fx_unit, fy_unit = _DIR_UNIT.get(req.forceDir, (0.0, -1.0))214 215    load_specs    = [{"type": "surface", "face": load_face_full,216                      "fx": fx_unit * req.force, "fy": fy_unit * req.force}]217    support_specs = [{"type": "fixed", "face": fixed_face_full}]218 219    load_cases, fixed_dofs, F_mag = build_load_cases(load_specs, support_specs, nelx, nely)220 221    # ── Run SIMP ───────────────────────────────────────────────────────────────222    xPhys, history, stress_field = simp_core(223        nelx=nelx, nely=nely,224        volfrac=req.vf,225        nu=nu,226        penal=3.0,227        H=H, Hs=Hs,228        max_iter=max_iter,229        load_cases=load_cases,230        fixed_dofs=fixed_dofs,231    )232 233    # ── Metrics ────────────────────────────────────────────────────────────────234    bimodality = _compute_bimodality(xPhys)235    non_convergent = _check_non_convergent(history)236    has_load_path = _check_load_path(xPhys, fixed_face_full, load_face_full)237 238    # Physical stress: σ_phys [MPa] = σ_norm × F_mag [N] / (dx_mm × thickness_mm)239    dx_mm = req.w / nelx240    sigma_phys_max = float(stress_field.max()) * F_mag / (dx_mm * req.d)241    yield_exceeded = sigma_phys_max * req.sf > mat["yield_strength_mpa"]242 243    # Mass: volume × vf × density244    mass_g = round(float(req.w * req.h * req.d * req.vf * mat["rho_gcc"]) / 1000.0, 2)245 246    # Print time estimate247    print_est = estimate_print(248        mass_g=mass_g,249        rho_gcc=mat["rho_gcc"],250        cost_per_kg=mat.get("cost_per_kg", 20.0),251    )252 253    # ── STL ────────────────────────────────────────────────────────────────────254    stl_bytes, _n_faces = to_stl_bytes(xPhys, req.w, req.h, req.d, iso=0.45)255 256    run_id = str(uuid.uuid4())[:8]257    _ensure_dirs()258    stl_available = False259    if stl_bytes:260        (STL_DIR / f"{run_id}.stl").write_bytes(stl_bytes)261        stl_available = True262 263    # ── Persist thin run record (no large arrays) ─────────────────────────────264    run_meta = {265        "id":            run_id,266        "label":         f"{req.material} · {int(req.w)}×{int(req.h)}×{int(req.d)}",267        "material":      req.material,268        "w": req.w, "h": req.h, "d": req.d,269        "fixedFace":     req.fixedFace,270        "loadFace":      req.loadFace,271        "forceDir":      req.forceDir,272        "force":         req.force,273        "vf":            req.vf,274        "sf":            req.sf,275        "fidelity":      req.fidelity,276        "infill":        req.infill,277        "compliance":    round(float(history[-1]), 3) if history else 0.0,278        "max_stress":    round(sigma_phys_max, 2),279        "iterations":    len(history),280        "mass_g":        mass_g,281        "bimodality":    round(bimodality, 3),282        "stl_available": stl_available,283    }284    hist = _load_history()285    hist["runs"].insert(0, run_meta)286    hist["runs"] = hist["runs"][:20]287    _save_history(hist)288 289    # ── Full response (includes arrays for charts) ────────────────────────────290    return {291        **run_meta,292        "non_convergent":  non_convergent,293        "no_load_path":    not has_load_path,294        "yield_exceeded":  yield_exceeded,295        "print_time_h":    print_est["time_h"],296        "filament_cost":   print_est["material_cost_usd"],297        "convergence":     [round(v, 3) for v in history],298        "density_grid":    xPhys.tolist(),   # list[list[float]], shape nely × nelx299    }300 301 302@app.get("/api/runs")303def get_runs():304    return _load_history()305 306 307@app.get("/api/runs/{run_id}/stl")308def download_stl(run_id: str):309    stl_path = STL_DIR / f"{run_id}.stl"310    if not stl_path.exists():311        raise HTTPException(404, "STL not found — run the optimizer first")312    fname = f"morphai_{run_id}.stl"313    return Response(314        content=stl_path.read_bytes(),315        media_type="model/stl",316        headers={"Content-Disposition": f'attachment; filename="{fname}"'},317    )318 319 320# ── Serve React frontend (must be last — catches everything else) ──────────────321app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")322