harishaseebat92 commited on
Commit
37fa40d
·
verified ·
1 Parent(s): 8a4db24

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -59
app.py CHANGED
@@ -12,19 +12,30 @@ from pydantic import BaseModel
12
  def solve_3d_heat_equation(Lx: float, Ly: float, Lz: float,
13
  t_max: float,
14
  Gamma: float = 0.1,
15
- Nx: int = 30, Ny: int = 30, Nz: int = 30, # Reduced default for 3D
16
  initial: str = "gaussian",
17
  bc: str = "dirichlet"):
18
  x = np.linspace(0, Lx, Nx)
19
  y = np.linspace(0, Ly, Ny)
20
  z = np.linspace(0, Lz, Nz)
21
- dx, dy, dz = x[1] - x, y[1] - y, z[1] - z # Corrected indexing
 
 
 
 
 
 
 
22
 
23
- if dx == 0 or dy == 0 or dz == 0:
24
- raise ValueError("Nx, Ny, and Nz must be > 1 for valid dx, dy, dz.")
 
 
 
 
25
 
26
  # Stability condition for 3D FTCS scheme
27
- dt = 0.5 / (Gamma * (1/dx**2 + 1/dy**2 + 1/dz**2)) # Adjusted for 3D
28
  Nt = int(np.ceil(t_max / dt)) + 1
29
 
30
  rx, ry, rz = Gamma * dt / dx**2, Gamma * dt / dy**2, Gamma * dt / dz**2
@@ -32,11 +43,13 @@ def solve_3d_heat_equation(Lx: float, Ly: float, Lz: float,
32
  X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
33
 
34
  if initial == "gaussian":
35
- u = np.exp(-(((X - Lx/2)**2 + (Y - Ly/2)**2 + (Z - Lz/2)**2) / (2*(Lx/10)**2)))
36
  elif initial == "random":
37
  u = np.random.rand(Nx, Ny, Nz)
38
  elif initial == "sinusoidal":
39
- kx, ky, kz = 2 * np.pi / Lx, 2 * np.pi / Ly, 2 * np.pi / Lz
 
 
40
  u = np.sin(kx * X) * np.sin(ky * Y) * np.sin(kz * Z)
41
  elif initial == "step":
42
  u = np.where((X < Lx/2) & (Y < Ly/2) & (Z < Lz/2), 1.0, 0.0)
@@ -48,31 +61,34 @@ def solve_3d_heat_equation(Lx: float, Ly: float, Lz: float,
48
 
49
  for n in range(1, Nt):
50
  un = u.copy()
51
- u[1:-1, 1:-1, 1:-1] = (
52
- un[1:-1, 1:-1, 1:-1]
53
- + rx * (un[2:, 1:-1, 1:-1] - 2 * un[1:-1, 1:-1, 1:-1] + un[:-2, 1:-1, 1:-1])
54
- + ry * (un[1:-1, 2:, 1:-1] - 2 * un[1:-1, 1:-1, 1:-1] + un[1:-1, :-2, 1:-1])
55
- + rz * (un[1:-1, 1:-1, 2:] - 2 * un[1:-1, 1:-1, 1:-1] + un[1:-1, 1:-1, :-2])
56
- )
 
 
 
 
 
 
 
 
 
57
 
58
  if bc == "dirichlet":
59
- u[0, :, :] = u[-1, :, :] = 0.0
60
- u[:, 0, :] = u[:, -1, :] = 0.0
61
- u[:, :, 0] = u[:, :, -1] = 0.0
62
  elif bc == "neumann": # Zero flux
63
- u[0, :, :] = u[1, :, :]
64
- u[-1, :, :] = u[-2, :, :]
65
- u[:, 0, :] = u[:, 1, :]
66
- u[:, -1, :] = u[:, -2, :]
67
- u[:, :, 0] = u[:, :, 1]
68
- u[:, :, -1] = u[:, :, -2]
69
  elif bc == "periodic":
70
- u[0, :, :] = un[-2, :, :]
71
- u[-1, :, :] = un[1, :, :]
72
- u[:, 0, :] = un[:, -2, :]
73
- u[:, -1, :] = un[:, 1, :]
74
- u[:, :, 0] = un[:, :, -2]
75
- u[:, :, -1] = un[:, :, 1]
76
  else:
77
  raise ValueError(f"Unknown bc: {bc}")
78
  U[n] = u.copy()
@@ -83,35 +99,74 @@ def create_animation_gif_3d_slice(U, Lx, Ly, Lz, initial, bc, Gamma, frame_skip,
83
  Nt, Nx, Ny, Nz = U.shape
84
  fig, ax = plt.subplots()
85
 
86
- slice_z_idx = Nz // 2
87
- z_coord_slice = np.linspace(0, Lz, Nz)[slice_z_idx]
 
88
 
89
- data_slice = U[0, :, :, slice_z_idx].T
 
 
 
 
 
 
 
 
 
 
 
90
 
 
 
 
 
 
91
  im = ax.imshow(data_slice, cmap='viridis', origin='lower',
92
- extent=[0, Lx, 0, Ly], vmin=U.min(), vmax=U.max())
93
  ax.set_title(f"3D Heat Eq (xy-slice at z={z_coord_slice:.2f})\ninit={initial}, bc={bc}, Gamma={Gamma:.2f}")
94
  ax.set_xlabel("x")
95
  ax.set_ylabel("y")
96
  plt.colorbar(im, ax=ax, label="u")
97
 
98
  def update(frame):
99
- im.set_data(U[frame, :, :, slice_z_idx].T)
 
 
 
100
  return [im]
101
 
102
- if Nt <= 1:
103
- idx = # Only one frame (initial state)
104
- else:
105
- idx = list(range(0, Nt, frame_skip))
106
- if not idx: # If frame_skip is too large for Nt > 1
107
- idx =
108
- if idx[-1]!= Nt - 1: # Ensure last frame is included
 
 
 
 
 
 
109
  idx.append(Nt - 1)
110
- # Remove duplicates if Nt-1 was already included by range
111
- idx = sorted(list(set(idx)))
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
 
114
- ani = FuncAnimation(fig, update, frames=idx, blit=True)
115
 
116
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmpfile:
117
  ani.save(tmpfile.name, writer='pillow', fps=max(1, 30 // max(1,frame_skip)))
@@ -123,21 +178,29 @@ def create_animation_gif_3d_slice(U, Lx, Ly, Lz, initial, bc, Gamma, frame_skip,
123
  # --- Plotly Figure Generator (3D Volume) ---
124
  def create_plotly_figure_3d(u_3d, Lx, Ly, Lz, time_label):
125
  Nx, Ny, Nz = u_3d.shape
 
 
 
126
  x_coords = np.linspace(0, Lx, Nx)
127
  y_coords = np.linspace(0, Ly, Ny)
128
  z_coords = np.linspace(0, Lz, Nz)
129
 
130
  X, Y, Z = np.meshgrid(x_coords, y_coords, z_coords, indexing='ij')
131
 
 
 
 
 
 
132
  fig = go.Figure(data=go.Volume(
133
  x=X.flatten(),
134
  y=Y.flatten(),
135
  z=Z.flatten(),
136
  value=u_3d.flatten(),
137
- isomin=u_3d.min(), # Use actual min/max of the current data
138
- isomax=u_3d.max(),
139
  opacity=0.1,
140
- surface_count=17, # Adjusted for potentially better performance/look
141
  colorscale='viridis'
142
  ))
143
  fig.update_layout(
@@ -146,7 +209,6 @@ def create_plotly_figure_3d(u_3d, Lx, Ly, Lz, time_label):
146
  xaxis_title='x',
147
  yaxis_title='y',
148
  zaxis_title='z',
149
- # Aspect ratio can be important for 3D visualization
150
  aspectmode='cube'
151
  )
152
  )
@@ -154,11 +216,16 @@ def create_plotly_figure_3d(u_3d, Lx, Ly, Lz, time_label):
154
 
155
  # --- Simulation Runner (Extracted Logic for 3D) ---
156
  def run_simulation_3d(lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_skip):
 
 
 
 
 
157
  U, dt = solve_3d_heat_equation(
158
  Lx=lx, Ly=ly, Lz=lz, t_max=t_max, Gamma=gamma,
159
  Nx=nx, Ny=ny, Nz=nz, initial=initial, bc=bc
160
  )
161
- Nt = U.shape # Corrected: U.shape is Nt
162
 
163
  idx0 = 0
164
  idx1 = round((Nt - 1) / 4) if Nt > 1 else 0
@@ -180,29 +247,34 @@ def run_simulation_3d(lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_s
180
 
181
  # --- Gradio Interface Logic (3D) ---
182
  def gradio_interface_3d(lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_skip):
183
- nx, ny, nz, frame_skip = int(nx), int(ny), int(nz), int(frame_skip)
184
- # Ensure frame_skip is at least 1
185
- frame_skip = max(1, frame_skip)
 
 
 
 
 
186
  gif_path, fig0, fig1, fig2, fig3 = run_simulation_3d(
187
- lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_skip
188
  )
189
  return gif_path, fig0, fig1, fig2, fig3
190
 
191
  # --- Gradio UI Layout (3D) ---
192
  with gr.Blocks(theme=gr.themes.Soft(), title="3D Heat Simulator") as demo:
193
- gr.Markdown("# 🔥 3D Heat Equation Simulator\nAdjust parameters and run the simulation. Animation shows a central xy-slice.")
194
  with gr.Row():
195
  with gr.Column(scale=1):
196
  gr.Markdown("## Domain & Grid")
197
  lx_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Lx")
198
  ly_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Ly")
199
  lz_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Lz")
200
- nx_slider = gr.Slider(3, 60, 20, 1, label="Nx (e.g., 20-40 for speed)") # Max reduced
201
- ny_slider = gr.Slider(3, 60, 20, 1, label="Ny (e.g., 20-40 for speed)") # Max reduced
202
- nz_slider = gr.Slider(3, 60, 20, 1, label="Nz (e.g., 20-40 for speed)") # Max reduced
203
 
204
  gr.Markdown("## Simulation")
205
- t_slider = gr.Slider(0.01, 1.0, 0.1, 0.01, label="t_max (e.g., 0.1-0.5)") # Max t_max reduced
206
  gamma_slider = gr.Slider(0.001, 1.0, 0.1, 0.001, label="Gamma")
207
 
208
  gr.Markdown("## Conditions")
@@ -239,7 +311,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="3D Heat Simulator") as demo:
239
  inputs=inputs_list,
240
  outputs=outputs_list,
241
  fn=gradio_interface_3d,
242
- cache_examples=False # Consider disabling cache if examples are slow or large
243
  )
244
 
245
  # --- FastAPI Setup for API Endpoint (3D) ---
@@ -262,8 +334,12 @@ class SimulationParams3D(BaseModel):
262
 
263
  @app.post("/simulate_3d")
264
  def simulate_3d_api(params: SimulationParams3D):
265
- # Ensure frame_skip is at least 1 for API calls too
266
  params.frame_skip = max(1, params.frame_skip)
 
 
 
 
 
267
  gif_path, fig0, fig1, fig2, fig3 = run_simulation_3d(**params.dict())
268
  with open(gif_path, "rb") as f:
269
  gif_data = base64.b64encode(f.read()).decode('utf-8')
 
12
  def solve_3d_heat_equation(Lx: float, Ly: float, Lz: float,
13
  t_max: float,
14
  Gamma: float = 0.1,
15
+ Nx: int = 30, Ny: int = 30, Nz: int = 30,
16
  initial: str = "gaussian",
17
  bc: str = "dirichlet"):
18
  x = np.linspace(0, Lx, Nx)
19
  y = np.linspace(0, Ly, Ny)
20
  z = np.linspace(0, Lz, Nz)
21
+
22
+ # Corrected dx, dy, dz calculation
23
+ if Nx > 1: dx = x[1] - x
24
+ else: dx = Lx # Or handle as an error / specific case if Nx=1
25
+ if Ny > 1: dy = y[1] - y
26
+ else: dy = Ly
27
+ if Nz > 1: dz = z[1] - z
28
+ else: dz = Lz
29
 
30
+ if dx == 0 or dy == 0 or dz == 0: # This check might need adjustment if Nx/Ny/Nz=1 is allowed and handled differently
31
+ # If Nx, Ny, or Nz is 1, dx, dy, or dz could be the length itself, or this indicates an issue.
32
+ # For FTCS, we need at least 3 points in a dimension to compute spatial derivatives,
33
+ # unless boundary conditions handle the 1-point or 2-point cases specifically.
34
+ # The current loop u[1:-1,...] assumes at least 3 points.
35
+ raise ValueError("Nx, Ny, and Nz must be > 1 for the current FTCS scheme. Or dx/dy/dz became zero.")
36
 
37
  # Stability condition for 3D FTCS scheme
38
+ dt = 0.5 / (Gamma * (1/dx**2 + 1/dy**2 + 1/dz**2))
39
  Nt = int(np.ceil(t_max / dt)) + 1
40
 
41
  rx, ry, rz = Gamma * dt / dx**2, Gamma * dt / dy**2, Gamma * dt / dz**2
 
43
  X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
44
 
45
  if initial == "gaussian":
46
+ u = np.exp(-(((X - Lx/2)**2 + (Y - Ly/2)**2 + (Z - Lz/2)**2) / (2*(max(Lx,Ly,Lz)/10)**2))) # Adjusted sigma
47
  elif initial == "random":
48
  u = np.random.rand(Nx, Ny, Nz)
49
  elif initial == "sinusoidal":
50
+ kx = 2 * np.pi / Lx if Lx > 0 else 0
51
+ ky = 2 * np.pi / Ly if Ly > 0 else 0
52
+ kz = 2 * np.pi / Lz if Lz > 0 else 0
53
  u = np.sin(kx * X) * np.sin(ky * Y) * np.sin(kz * Z)
54
  elif initial == "step":
55
  u = np.where((X < Lx/2) & (Y < Ly/2) & (Z < Lz/2), 1.0, 0.0)
 
61
 
62
  for n in range(1, Nt):
63
  un = u.copy()
64
+ # Check if dimensions are large enough for slicing
65
+ if Nx > 2 and Ny > 2 and Nz > 2:
66
+ u[1:-1, 1:-1, 1:-1] = (
67
+ un[1:-1, 1:-1, 1:-1]
68
+ + rx * (un[2:, 1:-1, 1:-1] - 2 * un[1:-1, 1:-1, 1:-1] + un[:-2, 1:-1, 1:-1])
69
+ + ry * (un[1:-1, 2:, 1:-1] - 2 * un[1:-1, 1:-1, 1:-1] + un[1:-1, :-2, 1:-1])
70
+ + rz * (un[1:-1, 1:-1, 2:] - 2 * un[1:-1, 1:-1, 1:-1] + un[1:-1, 1:-1, :-2])
71
+ )
72
+ else:
73
+ # If any dimension is too small for the [1:-1] slice,
74
+ # the heat equation update needs to be handled differently (e.g. only boundaries apply)
75
+ # For simplicity, we assume Nx,Ny,Nz >=3 for internal point updates.
76
+ # Otherwise, u remains largely unchanged except by boundary conditions.
77
+ pass
78
+
79
 
80
  if bc == "dirichlet":
81
+ if Nx > 0: u[0, :, :] = u[-1, :, :] = 0.0
82
+ if Ny > 0: u[:, 0, :] = u[:, -1, :] = 0.0
83
+ if Nz > 0: u[:, :, 0] = u[:, :, -1] = 0.0
84
  elif bc == "neumann": # Zero flux
85
+ if Nx > 1: u[0, :, :] = u[1, :, :]; u[-1, :, :] = u[-2, :, :]
86
+ if Ny > 1: u[:, 0, :] = u[:, 1, :]; u[:, -1, :] = u[:, -2, :]
87
+ if Nz > 1: u[:, :, 0] = u[:, :, 1]; u[:, :, -1] = u[:, :, -2]
 
 
 
88
  elif bc == "periodic":
89
+ if Nx > 1: u[0, :, :] = un[-2, :, :]; u[-1, :, :] = un[1, :, :]
90
+ if Ny > 1: u[:, 0, :] = un[:, -2, :]; u[:, -1, :] = un[:, 1, :]
91
+ if Nz > 1: u[:, :, 0] = un[:, :, -2]; u[:, :, -1] = un[:, :, 1]
 
 
 
92
  else:
93
  raise ValueError(f"Unknown bc: {bc}")
94
  U[n] = u.copy()
 
99
  Nt, Nx, Ny, Nz = U.shape
100
  fig, ax = plt.subplots()
101
 
102
+ # Ensure Nz is valid for slicing
103
+ slice_z_idx = Nz // 2 if Nz > 0 else 0
104
+ z_coord_slice = np.linspace(0, Lz, Nz)[slice_z_idx] if Nz > 0 else 0
105
 
106
+ if Nt == 0: # Should not happen if solve_3d_heat_equation guarantees Nt >= 1
107
+ # Create a blank image or raise an error
108
+ # For now, let's assume Nt >= 1 based on solve_3d_heat_equation
109
+ # If this occurs, U.min()/max() and U will fail.
110
+ # Returning a placeholder or erroring out early is better.
111
+ # This path indicates an issue upstream.
112
+ # For robustness, one might return a path to a pre-made "error" GIF.
113
+ # However, given Nt = ceil(t_max/dt) + 1, Nt is always >= 1.
114
+ pass
115
+
116
+
117
+ data_slice = U[0, :, :, slice_z_idx].T if Nt > 0 and Nz > 0 else np.zeros((Ny, Nx)) # Handle empty slice
118
 
119
+ # Handle U being potentially empty or having non-finite values for min/max
120
+ vmin_val = U.min() if Nt > 0 and U.size > 0 and np.all(np.isfinite(U)) else 0
121
+ vmax_val = U.max() if Nt > 0 and U.size > 0 and np.all(np.isfinite(U)) else 1
122
+ if vmin_val == vmax_val: vmax_val = vmin_val + 1 # Avoid error in imshow if all values are same
123
+
124
  im = ax.imshow(data_slice, cmap='viridis', origin='lower',
125
+ extent=[0, Lx, 0, Ly], vmin=vmin_val, vmax=vmax_val)
126
  ax.set_title(f"3D Heat Eq (xy-slice at z={z_coord_slice:.2f})\ninit={initial}, bc={bc}, Gamma={Gamma:.2f}")
127
  ax.set_xlabel("x")
128
  ax.set_ylabel("y")
129
  plt.colorbar(im, ax=ax, label="u")
130
 
131
  def update(frame):
132
+ if Nz > 0:
133
+ im.set_data(U[frame, :, :, slice_z_idx].T)
134
+ else: # Should not happen if Nz is reasonably set
135
+ im.set_data(np.zeros((Ny, Nx))) # Placeholder for empty Z dimension
136
  return [im]
137
 
138
+ # Corrected idx generation
139
+ if Nt == 0:
140
+ idx =
141
+ elif Nt == 1:
142
+ idx =
143
+ else: # Nt > 1
144
+ current_frame_skip = max(1, frame_skip) # Ensure frame_skip is at least 1
145
+ idx = list(range(0, Nt, current_frame_skip))
146
+
147
+ if 0 not in idx : # Should always be true for range(0,...) unless Nt=0
148
+ idx.insert(0,0)
149
+
150
+ if (Nt - 1) not in idx:
151
  idx.append(Nt - 1)
152
+
153
+ idx = sorted(list(set(idx)))
154
+
155
+ # FuncAnimation requires at least one frame. If Nt=0, idx is empty.
156
+ # solve_3d_heat_equation ensures Nt >= 1, so idx will have at least .
157
+ if not idx and Nt > 0: # Fallback, though current logic should prevent this if Nt > 0
158
+ idx =
159
+
160
+ if not idx: # If Nt is 0 and idx is empty
161
+ # Create a dummy animation or return a placeholder path
162
+ # For now, this will likely cause FuncAnimation to error if idx is empty.
163
+ # However, as Nt >= 1, idx should not be empty.
164
+ # If it could be, one might do:
165
+ # if not idx: fig.savefig(tmp_path_for_static_image); return tmp_path
166
+ pass
167
 
168
 
169
+ ani = FuncAnimation(fig, update, frames=idx if idx else , blit=True) # Ensure frames is not empty
170
 
171
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmpfile:
172
  ani.save(tmpfile.name, writer='pillow', fps=max(1, 30 // max(1,frame_skip)))
 
178
  # --- Plotly Figure Generator (3D Volume) ---
179
  def create_plotly_figure_3d(u_3d, Lx, Ly, Lz, time_label):
180
  Nx, Ny, Nz = u_3d.shape
181
+ if Nx==0 or Ny==0 or Nz==0: # Handle empty u_3d
182
+ return go.Figure(layout_title_text=f"3D Heat Distribution (No Data) at t={time_label}")
183
+
184
  x_coords = np.linspace(0, Lx, Nx)
185
  y_coords = np.linspace(0, Ly, Ny)
186
  z_coords = np.linspace(0, Lz, Nz)
187
 
188
  X, Y, Z = np.meshgrid(x_coords, y_coords, z_coords, indexing='ij')
189
 
190
+ # Ensure u_3d is finite for min/max
191
+ vmin = np.min(u_3d[np.isfinite(u_3d)]) if np.any(np.isfinite(u_3d)) else 0
192
+ vmax = np.max(u_3d[np.isfinite(u_3d)]) if np.any(np.isfinite(u_3d)) else 1
193
+ if vmin == vmax: vmax = vmin + 0.1 # Avoid issues with single value
194
+
195
  fig = go.Figure(data=go.Volume(
196
  x=X.flatten(),
197
  y=Y.flatten(),
198
  z=Z.flatten(),
199
  value=u_3d.flatten(),
200
+ isomin=vmin,
201
+ isomax=vmax,
202
  opacity=0.1,
203
+ surface_count=17,
204
  colorscale='viridis'
205
  ))
206
  fig.update_layout(
 
209
  xaxis_title='x',
210
  yaxis_title='y',
211
  zaxis_title='z',
 
212
  aspectmode='cube'
213
  )
214
  )
 
216
 
217
  # --- Simulation Runner (Extracted Logic for 3D) ---
218
  def run_simulation_3d(lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_skip):
219
+ # Ensure grid dimensions are at least 3 for FTCS internal points, or handle 1D/2D cases if needed
220
+ nx = max(3, int(nx))
221
+ ny = max(3, int(ny))
222
+ nz = max(3, int(nz))
223
+
224
  U, dt = solve_3d_heat_equation(
225
  Lx=lx, Ly=ly, Lz=lz, t_max=t_max, Gamma=gamma,
226
  Nx=nx, Ny=ny, Nz=nz, initial=initial, bc=bc
227
  )
228
+ Nt = U.shape
229
 
230
  idx0 = 0
231
  idx1 = round((Nt - 1) / 4) if Nt > 1 else 0
 
247
 
248
  # --- Gradio Interface Logic (3D) ---
249
  def gradio_interface_3d(lx, ly, lz, t_max, gamma, nx, ny, nz, initial, bc, frame_skip):
250
+ nx_int, ny_int, nz_int = int(nx), int(ny), int(nz)
251
+ frame_skip_int = max(1, int(frame_skip))
252
+
253
+ # Ensure minimal grid dimensions for the current simulation logic
254
+ nx_int = max(3, nx_int)
255
+ ny_int = max(3, ny_int)
256
+ nz_int = max(3, nz_int)
257
+
258
  gif_path, fig0, fig1, fig2, fig3 = run_simulation_3d(
259
+ lx, ly, lz, t_max, gamma, nx_int, ny_int, nz_int, initial, bc, frame_skip_int
260
  )
261
  return gif_path, fig0, fig1, fig2, fig3
262
 
263
  # --- Gradio UI Layout (3D) ---
264
  with gr.Blocks(theme=gr.themes.Soft(), title="3D Heat Simulator") as demo:
265
+ gr.Markdown("# 🔥 3D Heat Equation Simulator\nAdjust parameters and run the simulation. Animation shows a central xy-slice. Grid (Nx,Ny,Nz) min 3.")
266
  with gr.Row():
267
  with gr.Column(scale=1):
268
  gr.Markdown("## Domain & Grid")
269
  lx_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Lx")
270
  ly_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Ly")
271
  lz_slider = gr.Slider(0.1, 5.0, 1.0, 0.1, label="Lz")
272
+ nx_slider = gr.Slider(3, 60, 20, 1, label="Nx (min 3, e.g., 20-40)")
273
+ ny_slider = gr.Slider(3, 60, 20, 1, label="Ny (min 3, e.g., 20-40)")
274
+ nz_slider = gr.Slider(3, 60, 20, 1, label="Nz (min 3, e.g., 20-40)")
275
 
276
  gr.Markdown("## Simulation")
277
+ t_slider = gr.Slider(0.01, 1.0, 0.1, 0.01, label="t_max (e.g., 0.1-0.5)")
278
  gamma_slider = gr.Slider(0.001, 1.0, 0.1, 0.001, label="Gamma")
279
 
280
  gr.Markdown("## Conditions")
 
311
  inputs=inputs_list,
312
  outputs=outputs_list,
313
  fn=gradio_interface_3d,
314
+ cache_examples=False
315
  )
316
 
317
  # --- FastAPI Setup for API Endpoint (3D) ---
 
334
 
335
  @app.post("/simulate_3d")
336
  def simulate_3d_api(params: SimulationParams3D):
 
337
  params.frame_skip = max(1, params.frame_skip)
338
+ # Ensure minimal grid dimensions for API calls too
339
+ params.nx = max(3, params.nx)
340
+ params.ny = max(3, params.ny)
341
+ params.nz = max(3, params.nz)
342
+
343
  gif_path, fig0, fig1, fig2, fig3 = run_simulation_3d(**params.dict())
344
  with open(gif_path, "rb") as f:
345
  gif_data = base64.b64encode(f.read()).decode('utf-8')