CoolFace
Apppublic

rishab1090/simulator

sourceHugging Faceunknownupdated 1y agoView on Hugging Face
0likes
app.py457 linesDownload Raw Back to root
1# app.py2import os, uuid, zipfile, math3from pathlib import Path4from typing import Optional5from fastapi import FastAPI, UploadFile, File, Form, HTTPException6from fastapi.responses import FileResponse7import numpy as np8import rasterio9from rasterio.transform import Affine10from scipy.ndimage import gaussian_filter, map_coordinates, binary_fill_holes, binary_closing, binary_dilation, generate_binary_structure11from scipy.ndimage import label12import matplotlib.pyplot as plt13from tqdm import tqdm14from shapely.geometry import LineString, mapping15import geopandas as gpd16from fastapi.responses import RedirectResponse17 18# gravity19g = 9.8120 21app = FastAPI(title="Runout simulator single-file API")22BASE_WORKDIR = Path("/tmp/runout_jobs_single")23 24BASE_WORKDIR.mkdir(parents=True, exist_ok=True)25 26# ---------------- IO helpers ----------------27def read_dem(path):28    with rasterio.open(path) as ds:29        dem = ds.read(1).astype(float)30        meta = ds.meta.copy()31        transform = ds.transform32        crs = ds.crs33    return dem, transform, crs, meta34 35def save_geotiff(arr, meta, path):36    meta2 = meta.copy()37    meta2.update(dtype="float32", count=1)38    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)39    with rasterio.open(path, "w", **meta2) as dst:40        dst.write(np.array(arr, dtype='float32'), 1)41 42def save_png(arr, out_path, cmap="inferno", smooth=1.0, log_scale=False, clip_percent=(0.1,99.9)):43    a = np.array(arr, dtype=float)44    a[~np.isfinite(a)] = np.nan45    if smooth and smooth > 0:46        a = gaussian_filter(a, sigma=smooth)47    vals = a[~np.isnan(a)]48    if vals.size == 0:49        a = np.zeros((10,10))50        vmin, vmax = 0,151    else:52        if log_scale:53            a = np.log1p(np.clip(a,0,None)*1e4)54            vals = a[~np.isnan(a)]55        vmin = float(np.percentile(vals, clip_percent[0]))56        vmax = float(np.percentile(vals, clip_percent[1]))57        if vmax <= vmin:58            vmax = vmin + 1e-659    os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)60    plt.imsave(out_path, a, cmap=cmap, vmin=vmin, vmax=vmax)61 62def save_samples_geojson(samples, transform, out_path, crs=None):63    feats=[]64    for i,path in enumerate(samples):65        if not path or len(path)<2: continue66        coords=[rasterio.transform.xy(transform,int(r),int(c)) for (r,c) in path]67        feats.append({"type":"Feature","geometry":mapping(LineString(coords)),"properties":{"id":i,"steps":len(coords)}})68    if not feats:69        return70    gdf = gpd.GeoDataFrame.from_features(feats)71    if crs is not None:72        try:73            gdf.set_crs(crs, inplace=True)74        except Exception:75            pass76    os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)77    gdf.to_file(out_path, driver="GeoJSON")78 79# ---------------- interpolation & gradients ----------------80def interp_bilinear(arr, pts):81    res = map_coordinates(arr, pts, order=1, mode='nearest')82    if np.size(res) == 1:83        return float(res)84    return res85 86def dem_gradients(dem, cellsize):87    # return dz/dx, dz/dy (rise per meter)88    dy, dx = np.gradient(dem, cellsize, cellsize)89    return dx, dy90 91def compute_slope_field(dx_field, dy_field):92    grad_mag = np.sqrt(dx_field*dx_field + dy_field*dy_field)93    slope_deg = np.degrees(np.arctan(grad_mag))94    return slope_deg95 96# ---------------- rim detection ----------------97def detect_rim_coords(dem, cellsize, pit_depth_frac=0.25, rim_buffer_m=6.0, min_rim_samples=500):98    dem_valid = np.where(np.isfinite(dem), dem, np.nan)99    zmin = float(np.nanmin(dem_valid)); zmax = float(np.nanmax(dem_valid))100    depth = zmax - zmin101    if depth <= 0:102        h,w = dem.shape103        return [(h//2,w//2)], depth104    pit_thresh = zmin + pit_depth_frac * depth105    pit_mask = (dem <= pit_thresh) & np.isfinite(dem)106    pit_mask = binary_fill_holes(pit_mask)107    pit_mask = binary_closing(pit_mask, structure=generate_binary_structure(2,2), iterations=2)108    lbls, nlab = label(pit_mask)109    if nlab > 1:110        counts = [(lbls==i).sum() for i in range(1, nlab+1)]111        largest = int(np.argmax(counts) + 1)112        pit_mask = (lbls == largest)113    iters = max(1, int(round(rim_buffer_m / cellsize)))114    rim_band = binary_dilation(pit_mask, iterations=iters) & (~pit_mask)115    coords = list(zip(*np.where(rim_band)))116    if len(coords) > min_rim_samples:117        step = max(1, int(len(coords)/min_rim_samples))118        coords = coords[::step]119    return coords, depth120 121# ---------------- steepest march ----------------122def march_steepest(dem, start_rc, cellsize, slope_deg, slope_thresh_deg, min_drop_m, max_steps):123    offs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]124    rows, cols = dem.shape125    r, c = int(start_rc[0]), int(start_rc[1])126    z0 = dem[r,c]127    visited = set(); visited.add((r,c))128    for step in range(max_steps):129        best = None; best_dz = 0.0130        z = dem[r,c]131        if not np.isfinite(z):132            break133        for (dr,dc) in offs:134            rr,cc = r+dr, c+dc135            if rr<0 or rr>=rows or cc<0 or cc>=cols: continue136            if (rr,cc) in visited: continue137            zn = dem[rr,cc]138            if not np.isfinite(zn): continue139            dz = z - zn140            if dz > best_dz:141                best_dz = dz; best = (rr,cc)142        if best is None or best_dz <= 0:143            break144        r,c = best; visited.add((r,c))145        cum_drop = z0 - dem[r,c]146        if slope_deg[r,c] >= slope_thresh_deg and cum_drop >= min_drop_m:147            return (r,c), cum_drop, step+1148    return None, 0.0, step+1149 150# ---------------- particle integrator with wall handling ----------------151def particle_run(dem, dx_field, dy_field, cellsize, start_xy_m, initial_speed, mu, step_loss,152                 transform, xi=1000.0, wall_slope_thresh_deg=60.0, wall_restitution=0.2,153                 wall_deflect_frac=0.85, stop_on_wall_ke=5.0,154                 dt_max=0.2, dt_min=0.005, max_steps=20000, v_threshold=0.2):155    x_m, y_m = start_xy_m156    rows, cols = dem.shape157    inv = ~transform158 159    # initial gradient / direction160    col0, row0 = inv * (x_m, y_m)161    pts0 = np.array([[row0],[col0]])162    try:163        gx0 = float(interp_bilinear(dx_field, pts0))164        gy0 = float(interp_bilinear(dy_field, pts0))165    except Exception:166        gx0 = 0.0; gy0 = 0.0167    downslope = np.array([-gx0, -gy0]); n0 = np.linalg.norm(downslope)168    dir_x, dir_y = (downslope / n0) if n0 > 1e-9 else (0.0, 0.0)169 170    vx = initial_speed * dir_x171    vy = initial_speed * dir_y172    pos_x = x_m; pos_y = y_m173 174    visited = []175    ke_cells = {}176 177    for step in range(max_steps):178        col_cur, row_cur = inv * (pos_x, pos_y)179        if col_cur < 0 or col_cur >= cols-1 or row_cur < 0 or row_cur >= rows-1:180            break181 182        pts = np.array([[row_cur],[col_cur]])183        try:184            gz_x = float(interp_bilinear(dx_field, pts))185            gz_y = float(interp_bilinear(dy_field, pts))186        except Exception:187            gz_x = 0.0; gz_y = 0.0188 189        grad_mag = math.hypot(gz_x, gz_y)190        local_slope_deg = math.degrees(math.atan(grad_mag))191 192        a_drive_x = -g * gz_x193        a_drive_y = -g * gz_y194 195        vmag = math.hypot(vx, vy)196        if vmag > 1e-9:197            ux = vx / vmag; uy = vy / vmag198            a_voellmy_mag = mu * g + (vmag * vmag) / max(1e-12, xi)199            a_voellmy_x = -a_voellmy_mag * ux200            a_voellmy_y = -a_voellmy_mag * uy201        else:202            a_voellmy_x = a_voellmy_y = 0.0203 204        c_drag = 0.03205        a_drag_x = -c_drag * vx * vmag206        a_drag_y = -c_drag * vy * vmag207 208        ax = a_drive_x + a_voellmy_x + a_drag_x209        ay = a_drive_y + a_voellmy_y + a_drag_y210 211        dt = min(dt_max, max(dt_min, 0.4 * (cellsize / (vmag + 1e-6))))212 213        vx_mid = vx + 0.5 * ax * dt214        vy_mid = vy + 0.5 * ay * dt215        new_pos_x = pos_x + vx_mid * dt216        new_pos_y = pos_y + vy_mid * dt217        new_vx = vx + ax * dt218        new_vy = vy + ay * dt219 220        # wall check at next location221        col_next, row_next = inv * (new_pos_x, new_pos_y)222        wall_encounter = False223        gz_x_n = gz_y_n = grad_mag_n = 0.0224        if 0 <= col_next < cols and 0 <= row_next < rows:225            try:226                gz_x_n = float(interp_bilinear(dx_field, np.array([[row_next],[col_next]])))227                gz_y_n = float(interp_bilinear(dy_field, np.array([[row_next],[col_next]])))228                grad_mag_n = math.hypot(gz_x_n, gz_y_n)229                local_slope_deg_n = math.degrees(math.atan(grad_mag_n))230            except Exception:231                local_slope_deg_n = 0.0232                grad_mag_n = 0.0233            if local_slope_deg_n >= wall_slope_thresh_deg:234                wall_encounter = True235                n_hat = np.array([gz_x_n, gz_y_n]) / max(1e-12, grad_mag_n)236 237        if wall_encounter:238            ke = 0.5 * (vmag * vmag)239            if ke <= stop_on_wall_ke:240                r_idx = int(round(row_cur)); c_idx = int(round(col_cur))241                if 0 <= r_idx < rows and 0 <= c_idx < cols:242                    visited.append((r_idx, c_idx))243                    ke_cells[(r_idx,c_idx)] = max(ke_cells.get((r_idx,c_idx), 0.0), ke)244                break245 246            t = np.array([-gz_y_n, gz_x_n])247            tnorm = np.linalg.norm(t)248            if tnorm < 1e-12:249                v_vec = np.array([new_vx, new_vy])250                nv = np.dot(v_vec, n_hat)251                v_reflect = v_vec - (1.0 + wall_restitution) * nv * n_hat252                v_reflect *= 0.9253                new_vx, new_vy = float(v_reflect[0]), float(v_reflect[1])254            else:255                t_hat = t / tnorm256                v_vec = np.array([new_vx, new_vy])257                v_tang = np.dot(v_vec, t_hat) * t_hat258                v_norm = v_vec - v_tang259                new_v_vec = wall_deflect_frac * v_tang - wall_restitution * v_norm260                new_vx, new_vy = float(new_v_vec[0]), float(new_v_vec[1])261                new_pos_x = pos_x + new_vx * dt * 0.6262                new_pos_y = pos_y + new_vy * dt * 0.6263 264            vmag_after = math.hypot(new_vx, new_vy)265            if vmag_after < v_threshold:266                r_idx = int(round(row_cur)); c_idx = int(round(col_cur))267                if 0 <= r_idx < rows and 0 <= c_idx < cols:268                    visited.append((r_idx, c_idx))269                    ke_cells[(r_idx,c_idx)] = max(ke_cells.get((r_idx,c_idx), 0.0), 0.5 * vmag_after * vmag_after)270                break271 272            pos_x, pos_y = new_pos_x, new_pos_y273            vx, vy = new_vx, new_vy274        else:275            pos_x, pos_y = new_pos_x, new_pos_y276            vx, vy = new_vx, new_vy277 278        loss = np.clip(np.random.normal(loc=step_loss, scale=step_loss*0.2), 0.0, 0.6)279        vx *= (1.0 - loss); vy *= (1.0 - loss)280        vmag = math.hypot(vx, vy)281 282        col_round, row_round = inv * (pos_x, pos_y)283        r_idx = int(round(row_round)); c_idx = int(round(col_round))284        if r_idx < 0 or r_idx >= rows or c_idx < 0 or c_idx >= cols:285            break286        visited.append((r_idx, c_idx))287        ke_local = 0.5 * (vmag * vmag)288        ke_cells[(r_idx, c_idx)] = max(ke_cells.get((r_idx, c_idx), 0.0), ke_local)289 290        if vmag < v_threshold:291            break292 293    unique_visited = []294    seen = set()295    for rc in visited:296        if rc not in seen:297            unique_visited.append(rc); seen.add(rc)298    return unique_visited, ke_cells299 300# ---------------- Monte Carlo ----------------301def run_mc(dem, transform, release_cells, cellsize, trials, mu_mean, mu_std, xi, step_loss,302           min_drop, force_drop, sample_paths, seed, wall_slope_thresh_deg, wall_restitution, wall_deflect_frac, stop_on_wall_ke):303    rng = np.random.default_rng(seed)304    dx_field, dy_field = dem_gradients(dem, cellsize)305    heat = np.zeros_like(dem, dtype=float)306    impact = np.zeros_like(dem, dtype=float)307    sample_paths_out = []308    n_releases = len(release_cells)309    if n_releases == 0:310        raise RuntimeError("No release cells provided.")311    for t in tqdm(range(trials), desc="Trials"):312        r,c = release_cells[rng.integers(0, n_releases)]313        x0, y0 = rasterio.transform.xy(transform, int(r), int(c))314        if force_drop is not None:315            drop_m = float(force_drop)316        else:317            drop_m = max(min_drop, 0.02 * (np.nanmax(dem) - np.nanmin(dem)))318        initial_speed = math.sqrt(2.0 * g * drop_m)319        mu = float(max(0.0, rng.normal(mu_mean, mu_std)))320        path, ke_map = particle_run(dem, dx_field, dy_field, cellsize,321                                    (x0, y0), initial_speed, mu,322                                    step_loss, transform,323                                    xi=xi,324                                    wall_slope_thresh_deg=wall_slope_thresh_deg,325                                    wall_restitution=wall_restitution,326                                    wall_deflect_frac=wall_deflect_frac,327                                    stop_on_wall_ke=stop_on_wall_ke)328        for (rr, cc) in path:329            heat[rr, cc] += 1.0330            if (rr,cc) in ke_map:331                impact[rr, cc] = max(impact[rr, cc], ke_map[(rr,cc)])332        if len(sample_paths_out) < sample_paths:333            sample_paths_out.append(path)334    heat /= float(max(1, trials))335    return heat, impact, sample_paths_out336 337# ---------------- utils ----------------338def print_stats(name, arr):339    a = np.array(arr, dtype=float)340    a[~np.isfinite(a)] = np.nan341    non = a[~np.isnan(a)]342    if non.size == 0:343        return f"[STAT] {name}: no finite values"344    return f"[STAT] {name}: min={np.nanmin(non):.6g}, p1={np.nanpercentile(non,1):.6g}, p50={np.nanpercentile(non,50):.6g}, p99={np.nanpercentile(non,99):.6g}, max={np.nanmax(non):.6g}, mean={np.nanmean(non):.6g}"345 346# ---------------- API endpoint ----------------347@app.post("/run")348async def run_endpoint(349    dem: UploadFile = File(...),350    trials: int = Form(500),351    release_density: int = Form(1),352    pit_depth_frac_release: float = Form(0.25),353    slope_thresh_deg: float = Form(5.0),354    mu_mean: float = Form(0.05),355    mu_std: float = Form(0.01),356    xi: float = Form(1000.0),357    step_loss: float = Form(0.001),358    sample_paths: int = Form(50),359    wall_slope_thresh_deg: float = Form(60.0),360    wall_restitution: float = Form(0.2),361    wall_deflect_frac: float = Form(0.85),362    stop_on_wall_ke: float = Form(5.0),363    seed: int = Form(0),364    out_prefix: Optional[str] = Form(None),365):366    job_id = uuid.uuid4().hex[:12]367    job_dir = BASE_WORKDIR / job_id368    job_dir.mkdir(parents=True, exist_ok=True)369    dem_path = job_dir / "input_dem.tif"370    with dem_path.open("wb") as f:371        f.write(await dem.read())372    # read dem373    try:374        dem_arr, transform, crs, meta = read_dem(str(dem_path))375    except Exception as e:376        raise HTTPException(status_code=400, detail=f"Failed reading DEM: {e}")377    cellsize = abs(transform.a)378    # detect rim379    rim_coords, pit_depth = detect_rim_coords(dem_arr, cellsize, pit_depth_frac=pit_depth_frac_release, rim_buffer_m=6.0)380    dx_field, dy_field = dem_gradients(dem_arr, cellsize)381    slope_field = compute_slope_field(dx_field, dy_field)382    # march steepest to find release targets383    release_cells=[]384    for rc in rim_coords:385        tgt, cum_drop, steps = march_steepest(dem_arr, rc, cellsize, slope_field, slope_thresh_deg, 2.0, 400)386        if tgt is not None:387            release_cells.append(tgt)388    seeds = release_cells * max(1, int(release_density))389    if len(seeds) == 0:390        raise HTTPException(status_code=400, detail="No release seeds found.")391    # debug rim png392    debug_rim_png = job_dir / f"{job_id}_rim_debug.png"393    try:394        overlay = np.zeros((dem_arr.shape[0], dem_arr.shape[1], 3), dtype=float)395        dmmin = float(np.nanmin(dem_arr)); dmmax = float(np.nanmax(dem_arr))396        norm = (dem_arr - dmmin) / (dmmax - dmmin + 1e-9)397        overlay[..., :] = np.expand_dims(norm,2) * 0.35398        for (r,c) in release_cells:399            if 0 <= r < overlay.shape[0] and 0 <= c < overlay.shape[1]:400                overlay[r,c] = [1.0, 0.0, 0.0]401        plt.imsave(str(debug_rim_png), np.clip(overlay,0,1))402    except Exception:403        pass404    # run Monte Carlo405    try:406        heat, impact, sample_paths = run_mc(dem_arr, transform, seeds, cellsize,407                                            int(trials), float(mu_mean), float(mu_std), float(xi), float(step_loss),408                                            float(2.0), None, int(sample_paths), int(seed),409                                            float(wall_slope_thresh_deg), float(wall_restitution), float(wall_deflect_frac), float(stop_on_wall_ke))410    except Exception as e:411        raise HTTPException(status_code=500, detail=f"Simulation error: {e}")412    # outputs413    prefix = out_prefix or job_id414    out_heat_png = job_dir / f"{prefix}_heat.png"415    out_impact_png = job_dir / f"{prefix}_impact.png"416    out_heat_tif = job_dir / f"{prefix}_heat.tif"417    out_impact_tif = job_dir / f"{prefix}_impact.tif"418    out_samples_geojson = job_dir / f"{prefix}_samples.geojson"419    try:420        save_geotiff(heat, meta, str(out_heat_tif))421        save_geotiff(impact, meta, str(out_impact_tif))422        save_png(heat, str(out_heat_png), cmap="inferno", smooth=1.0, log_scale=False)423        save_png(impact, str(out_impact_png), cmap="magma", smooth=1.0, log_scale=True)424        save_samples_geojson(sample_paths, transform, str(out_samples_geojson), crs=crs)425    except Exception as e:426        raise HTTPException(status_code=500, detail=f"Failed to write outputs: {e}")427    # diagnostics text428    diag_txt = job_dir / "diagnostics.txt"429    with diag_txt.open("w") as f:430        f.write(f"Rim pixels: {len(rim_coords)}  pit_depth: {pit_depth}\n")431        f.write(print_stats("heat", heat) + "\n")432        f.write(print_stats("impact", impact) + "\n")433        try:434            idx = np.nanargmax(impact)435            r = int(idx) // impact.shape[1]; c = int(idx) % impact.shape[1]436            x,y = rasterio.transform.xy(transform, r, c)437            f.write(f"Max impact at row {r} col {c} -> x={x:.3f}, y={y:.3f}, KE={impact[r,c]:.6g}\n")438        except Exception:439            pass440    # zip outputs441    zip_path = job_dir / f"{job_id}_outputs.zip"442    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:443        for p in [out_heat_png, out_impact_png, out_heat_tif, out_impact_tif, out_samples_geojson, debug_rim_png, diag_txt]:444            if p.exists():445                zf.write(p, arcname=p.name)446    return FileResponse(path=str(zip_path), filename=zip_path.name, media_type="application/zip")447 448@app.get("/health")449def health():450    return {"status":"ok"}451 452 453@app.get("/")454def root():455    return RedirectResponse(url="/docs")456 457