Spaces:
Running
Running
| import matplotlib | |
| matplotlib.use('Agg') | |
| import numpy as np | |
| import scipy.ndimage as ndimage | |
| import matplotlib.pyplot as plt | |
| import matplotlib.animation as animation | |
| from matplotlib.patches import Circle | |
| from matplotlib.colors import LinearSegmentedColormap | |
| import gradio as gr | |
| import tempfile | |
| # ============================================================ | |
| # ПАРАМЕТРЫ СЕТКИ LBM И ЦВЕТОВАЯ ПАЛИТРА | |
| # ============================================================ | |
| N = 250 | |
| X, Y = np.meshgrid(np.arange(N), np.arange(N)) | |
| c = np.array([[0,0],[1,0],[0,1],[-1,0],[0,-1],[1,1],[-1,1],[-1,-1],[1,-1]], dtype=float) | |
| w = np.array([4/9] + [1/9]*4 + [1/36]*4, dtype=float) | |
| opp = np.array([0, 3, 4, 1, 2, 7, 8, 5, 6], dtype=int) | |
| cfd_multicolor_cmap = LinearSegmentedColormap.from_list("cfd_multicolor", | |
| [(0.00,"#004d99"), (0.10, "#00b7ff"), (0.25, "#00ffd0"), (0.40, "#00ff66"), | |
| (0.60, "#dfff00"), (0.85, "#ff9a00"), (1.00, "#ff2200")], N=512) | |
| cfd_multicolor_cmap.set_bad(color='#1c1c1c') | |
| def equilibrium(rho, u): | |
| feq = np.zeros((9, N, N), dtype=float) | |
| usqr = u[0]**2 + u[1]**2 | |
| for i in range(9): | |
| cu = 3.0 * (c[i,0] * u[0] + c[i,1] * u[1]) | |
| feq[i] = rho * w[i] * (1.0 + cu + 0.5*cu**2 - 1.5*usqr) | |
| return feq | |
| def equilibrium_1d(rho_1d, u_1d): | |
| feq = np.zeros((9, len(rho_1d)), dtype=float) | |
| usqr = u_1d[0]**2 + u_1d[1]**2 | |
| for i in range(9): | |
| cu = 3.0 * (c[i,0] * u_1d[0] + c[i,1] * u_1d[1]) | |
| feq[i] = rho_1d * w[i] * (1.0 + cu + 0.5*cu**2 - 1.5*usqr) | |
| return feq | |
| # ============================================================ | |
| # ГЕОМЕТРИЯ: ЛАГРАНЖЕВЫЕ ЯЧЕЙКИ + ЭЙЛЕРОВА МАСКА | |
| # ============================================================ | |
| def get_fish_geometry(cx, cy, theta, phase, L=65.0, d_max=9.0, n_pts=50): | |
| s = np.linspace(0, L, n_pts) | |
| envelope = (0.04 + 0.22 * (s / L)**2) * L | |
| y_mid = envelope * np.sin(2.0 * np.pi * (s / L - phase)) | |
| ds = L / (n_pts - 1) | |
| dy_mid_ds = np.gradient(y_mid, ds) | |
| dx_mid_ds = np.ones_like(dy_mid_ds) | |
| mag = np.hypot(dx_mid_ds, dy_mid_ds) + 1e-10 | |
| nx_loc = -dy_mid_ds / mag | |
| ny_loc = dx_mid_ds / mag | |
| s_norm = s / L | |
| h = d_max * np.sin(np.pi * (s_norm**0.6)) * (1.0 - 0.65 * s_norm) | |
| h = np.maximum(h, 1.0) | |
| x_loc = s - L / 2.0 | |
| xl_loc = x_loc + h * nx_loc | |
| yl_loc = y_mid + h * ny_loc | |
| xr_loc = x_loc - h * nx_loc | |
| yr_loc = y_mid - h * ny_loc | |
| cos_t, sin_t = np.cos(theta), np.sin(theta) | |
| def rot_trans(xl, yl): | |
| return cx + xl * cos_t - yl * sin_t, cy + xl * sin_t + yl * cos_t | |
| def rot_vec(vx, vy): | |
| return vx * cos_t - vy * sin_t, vx * sin_t + vy * cos_t | |
| x_c_glob, y_c_glob = rot_trans(x_loc, y_mid) | |
| X_left, Y_left = rot_trans(xl_loc, yl_loc) | |
| X_right, Y_right = rot_trans(xr_loc, yr_loc) | |
| NX_left, NY_left = rot_vec(nx_loc, ny_loc) | |
| NX_right, NY_right = rot_vec(-nx_loc, -ny_loc) | |
| X_bnd = np.concatenate([X_left, X_right[::-1]]) | |
| Y_bnd = np.concatenate([Y_left, Y_right[::-1]]) | |
| NX_bnd = np.concatenate([NX_left, NX_right[::-1]]) | |
| NY_bnd = np.concatenate([NY_left, NY_right[::-1]]) | |
| dx_grid = X[:, :, None] - x_c_glob | |
| dy_grid = Y[:, :, None] - y_c_glob | |
| dist_sq = dx_grid**2 + dy_grid**2 | |
| solid = np.any(dist_sq < (h**2), axis=-1) | |
| idx_nearest = np.argmin(dist_sq, axis=-1) | |
| eroded = ndimage.binary_erosion(solid, structure=np.ones((3, 3), dtype=bool)) | |
| boundary = solid & ~eroded | |
| return solid, boundary, x_c_glob, y_c_glob, idx_nearest, X_bnd, Y_bnd, NX_bnd, NY_bnd | |
| # ============================================================ | |
| # ОСНОВНОЙ ДВИЖОК | |
| # ============================================================ | |
| def simulate(frames=400, frequency=3.0, relative_density=1.0, tau=0.58, vis_mode="Pressure"): | |
| omega_lbm = 1.0 / tau | |
| rho = np.ones((N, N), dtype=float) | |
| u = np.zeros((2, N, N), dtype=float) | |
| f = equilibrium(rho, u) | |
| L_fish = 65.0 | |
| cm_x, cm_y = float(N) * 0.25, float(N) * 0.5 | |
| v_cm_x, v_cm_y = 0.0, 0.0 | |
| theta, omega_h = 0.0, 0.0 | |
| phase = 0.0 | |
| dt_lbm = 1.0 | |
| LBM_DT = 0.003 | |
| lbm_frequency = frequency * LBM_DT | |
| Ff_x, Ff_y, Tf = 0.0, 0.0, 0.0 | |
| solid_old, _, _, _, _, X_bnd_old, Y_bnd_old, _, _ = get_fish_geometry(cm_x, cm_y, theta, phase, L=L_fish) | |
| sponge_thickness = 20 | |
| sponge_sigma = np.zeros((N, N), dtype=float) | |
| for i in range(sponge_thickness): | |
| damping = ((sponge_thickness - i) / sponge_thickness)**2 * 0.15 | |
| sponge_sigma[i, :] = np.maximum(sponge_sigma[i, :], damping) | |
| sponge_sigma[-1-i, :] = np.maximum(sponge_sigma[-1-i, :], damping) | |
| sponge_sigma[:, i] = np.maximum(sponge_sigma[:, i], damping) | |
| sponge_sigma[:, -1-i] = np.maximum(sponge_sigma[:, -1-i], damping) | |
| f_rest = equilibrium(np.ones((N, N)), np.zeros((2, N, N))) | |
| fig, ax = plt.subplots(figsize=(8, 8), facecolor='#121212') | |
| ax.set_facecolor('#121212') | |
| ax.axis('off') | |
| fig.tight_layout() | |
| imgs = [] | |
| # -------------------------------------------------------- | |
| # ИНИЦИАЛИЗАЦИЯ: СКАЛЯРНАЯ ЭНЕРГИЯ И УСРЕДНЕННЫЙ ВЕКТОР НАПРАВЛЕНИЯ | |
| # -------------------------------------------------------- | |
| N_bnd = len(X_bnd_old) | |
| stored_energy = np.zeros(N_bnd, dtype=float) | |
| stored_dir_x = np.zeros(N_bnd, dtype=float) # Хранит усредненное направление X | |
| stored_dir_y = np.zeros(N_bnd, dtype=float) # Хранит усредненное направление Y | |
| P_old = np.zeros(N_bnd, dtype=float) | |
| for t in range(frames): | |
| phase_new = phase + lbm_frequency | |
| solid_new, boundary_new, x_pts, y_pts, idx_nearest, X_bnd, Y_bnd, NX_bnd, NY_bnd = get_fish_geometry( | |
| cm_x, cm_y, theta, phase_new, L=L_fish | |
| ) | |
| v_skin_x = (X_bnd - X_bnd_old) / dt_lbm | |
| v_skin_y = (Y_bnd - Y_bnd_old) / dt_lbm | |
| X_bnd_old, Y_bnd_old = np.copy(X_bnd), np.copy(Y_bnd) | |
| area_fish = np.sum(solid_new) | |
| m_fish = relative_density * area_fish | |
| M_eff = m_fish * 1.2 | |
| I_eff = 0.5 * m_fish * (L_fish / 3.5)**2 | |
| mask_fluid = ~solid_old | |
| rho_sum = np.sum(f, axis=0) | |
| u[0] = np.sum(f * c[:,0].reshape(9,1,1), axis=0) / (rho_sum + 1e-5) | |
| u[1] = np.sum(f * c[:,1].reshape(9,1,1), axis=0) / (rho_sum + 1e-5) | |
| max_fluid_u = 0.25 | |
| u[0] = np.clip(u[0], -max_fluid_u, max_fluid_u) | |
| u[1] = np.clip(u[1], -max_fluid_u, max_fluid_u) | |
| u[0, solid_old] = 0.0 | |
| u[1, solid_old] = 0.0 | |
| feq = equilibrium(rho_sum, u) | |
| f[:, mask_fluid] = f[:, mask_fluid] * (1 - omega_lbm) + omega_lbm * feq[:, mask_fluid] | |
| f = f * (1.0 - sponge_sigma) + f_rest * sponge_sigma | |
| for i in range(9): | |
| f[i] = np.roll(f[i], c[i].astype(int), axis=(1,0)) | |
| uncovered = solid_old & ~solid_new | |
| r_vec_x, r_vec_y = X - cm_x, Y - cm_y | |
| if np.any(uncovered): | |
| alpha = 0.25 | |
| ux_unc = alpha * v_cm_x + (1-alpha) * u[0][uncovered] | |
| uy_unc = alpha * v_cm_y + (1-alpha) * u[1][uncovered] | |
| rho_unc = np.ones_like(ux_unc) | |
| feq_refill = equilibrium_1d(rho_unc, np.array([ux_unc, uy_unc])) | |
| for i in range(9): | |
| f[i, uncovered] = feq_refill[i] | |
| F_fluid_x_raw, F_fluid_y_raw, tau_fluid_raw = 0.0, 0.0, 0.0 | |
| f_new = np.copy(f) | |
| y_coords, x_coords = np.where(boundary_new) | |
| for i in range(1, 9): | |
| o = opp[i] | |
| src_y = np.clip(y_coords - int(c[i, 1]), 0, N-1) | |
| src_x = np.clip(x_coords - int(c[i, 0]), 0, N-1) | |
| is_fluid_source = (~solid_old[src_y, src_x]) | (~solid_new[src_y, src_x]) | |
| valid_y = y_coords[is_fluid_source] | |
| valid_x = x_coords[is_fluid_source] | |
| if len(valid_y) == 0: continue | |
| rx_bb, ry_bb = r_vec_x[valid_y, valid_x], r_vec_y[valid_y, valid_x] | |
| vw_rot_x, vw_rot_y = -omega_h * ry_bb, omega_h * rx_bb | |
| near_idx = idx_nearest[valid_y, valid_x] | |
| vw_bend_x = v_skin_x[near_idx] | |
| vw_bend_y = v_skin_y[near_idx] | |
| v_wall_x = v_cm_x + vw_rot_x + vw_bend_x * 0.5 | |
| v_wall_y = v_cm_y + vw_rot_y + vw_bend_y * 0.5 | |
| cu_wall = 3.0 * (c[i,0] * v_wall_x + c[i,1] * v_wall_y) | |
| rho_wall = rho_sum[valid_y, valid_x] | |
| f_in = f[i, valid_y, valid_x] | |
| f_out = f_in - 2.0 * w[i] * rho_wall * cu_wall | |
| f_new[o, valid_y, valid_x] = f_out | |
| dp_x = (f_in + f_out) * c[i,0] | |
| dp_y = (f_in + f_out) * c[i,1] | |
| F_fluid_x_raw += np.sum(dp_x) | |
| F_fluid_y_raw += np.sum(dp_y) | |
| tau_fluid_raw += np.sum(rx_bb * dp_y - ry_bb * dp_x) | |
| f = f_new | |
| solid_old = np.copy(solid_new) | |
| phase = phase_new | |
| rho_sum = np.sum(f, axis=0) | |
| pressure = rho_sum - 1.0 | |
| # ============================================================ | |
| # ФИЗИЧЕСКИЙ РАСЧЕТ РАБОТЫ, ЭНЕРГИИ И АДВЕКЦИИ | |
| # ============================================================ | |
| offset = 1.5 | |
| X_sample = np.clip(X_bnd + offset * NX_bnd, 0, N-1) | |
| Y_sample = np.clip(Y_bnd + offset * NY_bnd, 0, N-1) | |
| P_bnd = ndimage.map_coordinates(pressure, [Y_sample, X_sample], order=1, mode='nearest') | |
| u_f_x = ndimage.map_coordinates(u[0], [Y_sample, X_sample], order=1, mode='nearest') | |
| u_f_y = ndimage.map_coordinates(u[1], [Y_sample, X_sample], order=1, mode='nearest') | |
| v_rel_x = u_f_x - v_skin_x | |
| v_rel_y = u_f_y - v_skin_y | |
| T_x = np.roll(X_bnd, -1) - np.roll(X_bnd, 1) | |
| T_y = np.roll(Y_bnd, -1) - np.roll(Y_bnd, 1) | |
| T_mag = np.hypot(T_x, T_y) + 1e-10 | |
| T_x /= T_mag | |
| T_y /= T_mag | |
| v_tangent = v_rel_x * T_x + v_rel_y * T_y | |
| ds_approx = (2.0 * L_fish) / N_bnd | |
| v_idx = v_tangent / ds_approx | |
| # Перенос энергии и направления работы вдоль контура | |
| mobility = 1.0 | |
| source_indices = (np.arange(N_bnd) - v_idx * mobility * dt_lbm) % N_bnd | |
| stored_energy = ndimage.map_coordinates(stored_energy, [source_indices], order=1, mode='wrap') | |
| stored_dir_x = ndimage.map_coordinates(stored_dir_x, [source_indices], order=1, mode='wrap') | |
| stored_dir_y = ndimage.map_coordinates(stored_dir_y, [source_indices], order=1, mode='wrap') | |
| P_old = ndimage.map_coordinates(P_old, [source_indices], order=1, mode='wrap') | |
| # Нормализуем направление после интерполяции, чтобы оно не затухало | |
| dir_norm = np.hypot(stored_dir_x, stored_dir_y) + 1e-12 | |
| stored_dir_x /= dir_norm | |
| stored_dir_y /= dir_norm | |
| # Сила давления жидкости НА рыбу | |
| Fx_p = -P_bnd * NX_bnd | |
| Fy_p = -P_bnd * NY_bnd | |
| power = Fx_p * v_skin_x + Fy_p * v_skin_y | |
| dW = power * dt_lbm | |
| # Направление воздействия рыбы НА воду (противоположно движению кожи) | |
| vmag_skin = np.hypot(v_skin_x, v_skin_y) + 1e-12 | |
| dir_x = -v_skin_x / vmag_skin | |
| dir_y = -v_skin_y / vmag_skin | |
| # Накопление: только когда P < 0 (формируется зона разрежения/вихрь) и работа положительна | |
| acc_mask = (P_bnd < 0.0) & (dW > 0.0) | |
| # Сначала запоминаем старую энергию для корректного усреднения вектора | |
| old_energy = stored_energy[acc_mask] | |
| stored_energy[acc_mask] += dW[acc_mask] | |
| # Взвешенное усреднение направления: не уничтожает скалярную энергию при противоположных импульсах | |
| new_vec_x = stored_dir_x[acc_mask] * old_energy + dir_x[acc_mask] * dW[acc_mask] | |
| new_vec_y = stored_dir_y[acc_mask] * old_energy + dir_y[acc_mask] * dW[acc_mask] | |
| norm_vec = np.hypot(new_vec_x, new_vec_y) + 1e-12 | |
| stored_dir_x[acc_mask] = new_vec_x / norm_vec | |
| stored_dir_y[acc_mask] = new_vec_y / norm_vec | |
| # Отдача: происходит только там, где есть энергия и давление начало расти (схлопывание) | |
| dP = P_bnd - P_old | |
| release_mask = (stored_energy > 1e-12) & (dP > 0.0) | |
| release_fraction = np.zeros_like(stored_energy) | |
| release_fraction[release_mask] = np.clip(dP[release_mask] / (np.abs(P_old[release_mask]) + 1e-8), 0.0, 1.0) | |
| released_energy = stored_energy * release_fraction | |
| # Отдаваемая сила направлена туда же, куда направлено усредненное воздействие на воду | |
| Fx_release = stored_dir_x * released_energy | |
| Fy_release = stored_dir_y * released_energy | |
| # Уменьшаем только скалярную энергию, вектор усредненного направления остается (он просто перестает действовать, когда E=0) | |
| stored_energy -= released_energy | |
| P_old = P_bnd.copy() | |
| # Превращение высвобожденной энергии в силу толчка (через F = E / dt) | |
| thrust_multiplier = 10.5 | |
| F_vortex_x = (np.sum(Fx_release) / dt_lbm) * thrust_multiplier | |
| F_vortex_y = (np.sum(Fy_release) / dt_lbm) * thrust_multiplier | |
| # ============================================================ | |
| alpha_force = 0.3 | |
| Ff_x += alpha_force * (F_fluid_x_raw - Ff_x) | |
| Ff_y += alpha_force * (F_fluid_y_raw - Ff_y) | |
| Tf += alpha_force * (tau_fluid_raw - Tf) | |
| Total_F_x = Ff_x + F_vortex_x | |
| Total_F_y = Ff_y + F_vortex_y | |
| v_cm_x += (Total_F_x / M_eff) * dt_lbm | |
| v_cm_y += (Total_F_y / M_eff) * dt_lbm | |
| max_v = 0.15 | |
| v_cm_x = np.clip(v_cm_x, -max_v, max_v) | |
| v_cm_y = np.clip(v_cm_y, -max_v, max_v) | |
| cm_x += v_cm_x * dt_lbm | |
| cm_y += v_cm_y * dt_lbm | |
| margin = L_fish * 0.6 | |
| cm_x = np.clip(cm_x, margin, N - margin) | |
| cm_y = np.clip(cm_y, margin, N - margin) | |
| omega_h += (Tf / I_eff) * dt_lbm | |
| theta += omega_h * dt_lbm | |
| # ============================================================ | |
| # ВИЗУАЛИЗАЦИЯ | |
| # ============================================================ | |
| if t % 2 == 0: | |
| if vis_mode == "Pressure": | |
| data = np.nan_to_num(pressure, nan=0.0) | |
| vabs = float(np.percentile(np.abs(data), 99.5)) or 1e-5 | |
| vmin, vmax = -vabs, vabs | |
| cmap = cfd_multicolor_cmap | |
| elif vis_mode == "Pressure Deviation": | |
| data = np.nan_to_num(np.abs(pressure), nan=0.0) | |
| vmax = float(np.percentile(data, 99.7)) or 1e-5 | |
| vmin = 0.0 | |
| cmap = cfd_multicolor_cmap | |
| data[solid_new] = np.nan | |
| im = ax.imshow(data, cmap=cmap, animated=True, vmin=vmin, vmax=vmax, interpolation='bilinear') | |
| frame_artists = [im] | |
| fish_line, = ax.plot(x_pts, y_pts, color='white', lw=1.8, animated=True, zorder=5) | |
| eye_x, eye_y = x_pts[7], y_pts[7] | |
| eye_patch = Circle((eye_x, eye_y), 1.8, color='white', animated=True, zorder=6) | |
| pupil_patch = Circle((eye_x, eye_y), 0.8, color='black', animated=True, zorder=7) | |
| energy_pts = ax.scatter(X_bnd, Y_bnd, c=stored_energy, cmap='Wistia', | |
| s=25, zorder=8, animated=True, vmin=0, vmax=0.05) | |
| ax.add_patch(eye_patch) | |
| ax.add_patch(pupil_patch) | |
| frame_artists.extend([fish_line, eye_patch, pupil_patch, energy_pts]) | |
| imgs.append(frame_artists) | |
| ani = animation.ArtistAnimation(fig, imgs, interval=40, blit=True) | |
| tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) | |
| ani.save(tmp.name, fps=25, extra_args=['-vcodec', 'libx264']) | |
| plt.close(fig) | |
| return tmp.name | |
| # ============================================================ | |
| # GRADIO ИНТЕРФЕЙС | |
| # ============================================================ | |
| with gr.Blocks() as demo: | |
| with gr.Column(elem_classes="border-class"): | |
| output = gr.Video(label="Fish Hydrodynamics: Physically Accurate Vector Advection", elem_classes="square-video-box") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("#### Global Params") | |
| frames = gr.Slider(minimum=50, maximum=2000, value=600, step=50, label="Frames") | |
| frequency = gr.Slider(minimum=0.1, maximum=10.0, value=3.0, step=0.1, label="Swimming Freq (Hz)") | |
| with gr.Column(): | |
| gr.Markdown("#### Hydrodynamics") | |
| relative_density = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1, label="Fish Density") | |
| tau = gr.Slider(minimum=0.51, maximum=0.95, value=0.58, step=0.01, label="Viscosity (Tau)") | |
| with gr.Column(): | |
| gr.Markdown("#### Visualization") | |
| vis_mode = gr.Radio( | |
| choices=["Pressure", "Pressure Deviation"], | |
| value="Pressure", label="Visualization Mode" | |
| ) | |
| btn = gr.Button("RUN SIMULATION", variant="primary") | |
| btn.click(simulate, inputs=[frames, frequency, relative_density, tau, vis_mode], outputs=output) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |