fargopy 0.3.14__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fargopy/fsimulation.py CHANGED
@@ -2,63 +2,207 @@ import pandas as pd
2
2
  import numpy as np
3
3
  import fargopy as fp
4
4
 
5
-
6
5
  import matplotlib.pyplot as plt
7
6
  import plotly.figure_factory as ff
8
7
  from plotly.subplots import make_subplots
9
8
  import plotly.graph_objects as go
10
9
  from matplotlib.animation import FFMpegWriter
11
10
 
12
- from ipywidgets import interact, FloatSlider,IntSlider
11
+ from ipywidgets import interact, FloatSlider, IntSlider
13
12
  from celluloid import Camera
14
- from IPython.display import HTML
15
- from IPython.display import Video
13
+ from IPython.display import HTML, Video
16
14
 
17
15
  from scipy.interpolate import griddata
18
16
  from scipy.integrate import solve_ivp
19
17
  from tqdm import tqdm
20
18
 
21
19
 
22
-
23
-
24
- class DataHandler:
20
+ class FieldInterpolate:
25
21
  def __init__(self, sim):
26
22
  self.sim = sim
27
23
  self.df = None
28
24
  self.plane = None
25
+ self.angle = None
26
+
27
+ def load_data(self, field=None, plane=None, angle=None, snapshots=None):
28
+ """
29
+ Loads data in 2D or 3D depending on the provided parameters.
30
+
31
+ Parameters:
32
+ field (list of str, optional): List of fields to load (e.g., ["gasdens", "gasv"]).
33
+ plane (str, optional): Plane for 2D data ('XZ', 'XY', 'YZ'). Required for 2D.
34
+ angle (float, optional): Angle for the 2D slice. Required for 2D.
35
+ snapshots (list or int, optional): List of snapshot indices or a single snapshot to load. Required for both 2D and 3D.
36
+ Returns:
37
+ pd.DataFrame: DataFrame containing the loaded data.
38
+ """
39
+ if field is None:
40
+ raise ValueError("You must specify at least one field to load using the 'fields' parameter.")
41
+
42
+ if (plane and not angle) or (angle and not plane):
43
+ raise ValueError("Both 'plane' and 'angle' must be provided for 2D data.")
44
+
45
+ if angle and not isinstance(angle, str):
46
+ raise ValueError("'angle' must be a str example: angle='theta=1.5' [rad]")
47
+
48
+ if not isinstance(snapshots, (int, list, tuple)):
49
+ raise ValueError("'snapshots' must be an integer, a list, or a tuple.")
50
+
51
+ if isinstance(snapshots, (list, tuple)) and len(snapshots) == 2:
52
+ if snapshots[0] > snapshots[1]:
53
+ raise ValueError("The range in 'snapshots' is invalid. The first value must be less than or equal to the second.")
54
+
55
+ if not hasattr(self.sim, "domains") or self.sim.domains is None:
56
+ raise ValueError("Simulation domains are not loaded. Ensure the simulation data is properly initialized.")
57
+
58
+ # Convert a single snapshot to a list
59
+ if isinstance(snapshots, int):
60
+ snapshots = [snapshots]
61
+
62
+ # Handle the case where snapshots is a single value or a list with one value
63
+ if len(snapshots) == 1:
64
+ snaps = snapshots
65
+ time_values = [0] # Single snapshot corresponds to a single time value
66
+ else:
67
+ snaps = np.arange(snapshots[0], snapshots[1] + 1)
68
+ time_values = np.linspace(0, 1, len(snaps))
29
69
 
30
- def load_data(self, plane, angle, num_snapshots):
31
- self.plane = plane
32
- snapshots = np.arange(1, num_snapshots + 1)
33
- time_values = snapshots / num_snapshots
34
-
35
- df_snapshots = pd.DataFrame(columns=["snapshot", "time", "vel1", "vel2", "gasdens", "coord1", "coord2"])
36
-
37
- for i, snap in enumerate(snapshots):
38
- gasv = self.sim.load_field('gasv', snapshot=snap, type='vector')
39
- gasvx, gasvy, gasvz = gasv.to_cartesian()
40
- gasd = self.sim.load_field('gasdens', snapshot=snap, type='scalar')
41
-
42
- if plane == 'XZ':
43
- vel1_slice, mesh = gasvx.meshslice(slice=angle)
44
- vel2_slice, _ = gasvz.meshslice(slice=angle)
45
- coord1, coord2 = mesh.x, mesh.z
46
- elif plane == 'XY':
47
- vel1_slice, mesh = gasvx.meshslice(slice=angle)
48
- vel2_slice, _ = gasvy.meshslice(slice=angle)
49
- coord1, coord2 = mesh.x, mesh.y
50
- elif plane == 'YZ':
51
- vel1_slice, mesh = gasvy.meshslice(slice='r=1.0')
52
- vel2_slice, _ = gasvz.meshslice(slice='r=1.0')
53
- coord1, coord2 = mesh.y, mesh.z
54
-
55
- gasd_slice, _ = gasd.meshslice(slice=angle)
56
- df_snapshots.loc[i] = [snap, time_values[i], vel1_slice, vel2_slice, gasd_slice, coord1, coord2]
57
-
58
- self.df = df_snapshots
59
- return df_snapshots
60
-
61
- def interpolate_field(self, time, var1, var2, field_name):
70
+ if plane and angle: # Load 2D data
71
+
72
+ # Map plane to coordinate names
73
+ plane_map = {
74
+ "XY": ("x", "y", "vx", "vy"),
75
+ "XZ": ("x", "z", "vx", "vz"),
76
+ "YZ": ("y", "z", "vy", "vz")
77
+ }
78
+
79
+ if plane not in plane_map:
80
+ raise ValueError(f"Invalid plane '{plane}'. Valid options are 'XY', 'XZ', 'YZ'.")
81
+
82
+ coord1, coord2, vel1, vel2 = plane_map[plane]
83
+
84
+ # Dynamically create DataFrame columns based on the fields
85
+ columns = ["snapshot", "time", coord1, coord2]
86
+ if field=="gasdens":
87
+ print(f"Loading 2D density data for plane {plane} at angle {angle} rad.")
88
+ columns.append("gasdens")
89
+ if field=="gasv":
90
+ columns.extend([vel1, vel2])
91
+ print(f"Loading 2D gas velocity data for plane {plane} at angle {angle} rad.")
92
+
93
+ if field=="gasenergy":
94
+ columns.append("gasenergy")
95
+ print(f"Loading 2D gas energy data for plane {plane} at angle {angle} rad.")
96
+ df_snapshots = pd.DataFrame(columns=columns)
97
+
98
+ for i, snap in enumerate(snaps):
99
+ row = {"snapshot": snap, "time": time_values[i]}
100
+
101
+ # Assign coordinates for all fields
102
+ gasv = self.sim.load_field('gasv', snapshot=snap, type='vector')
103
+ _, mesh = gasv.meshslice(slice=angle)
104
+ coord1_vals, coord2_vals = getattr(mesh, coord1), getattr(mesh, coord2)
105
+ row[coord1] = coord1_vals
106
+ row[coord2] = coord2_vals
107
+
108
+ if field=="gasdens" :
109
+ gasd = self.sim.load_field('gasdens', snapshot=snap, type='scalar')
110
+ gasd_slice, _ = gasd.meshslice(slice=angle)
111
+ row["gasdens"] = gasd_slice
112
+
113
+ if field=="gasv":
114
+ gasvx, gasvy, gasvz = gasv.to_cartesian()
115
+ vel1_slice, _ = getattr(gasvx, f"meshslice")(slice=angle)
116
+ vel2_slice, _ = getattr(gasvy, f"meshslice")(slice=angle)
117
+ row[vel1] = vel1_slice
118
+ row[vel2] = vel2_slice
119
+
120
+ if field=="gasenergy":
121
+ gasenergy = self.sim.load_field('gasenergy', snapshot=snap, type='scalar')
122
+ gasenergy_slice, _ = gasenergy.meshslice(slice=angle)
123
+ row["gasenergy"] = gasenergy_slice
124
+
125
+ # Convert the row to a DataFrame and concatenate it
126
+ row_df = pd.DataFrame([row])
127
+ df_snapshots = pd.concat([df_snapshots, row_df], ignore_index=True)
128
+
129
+ self.df = df_snapshots
130
+ return df_snapshots
131
+
132
+ elif plane is None and angle is None: # Load 3D data
133
+ print("Loading 3D data.")
134
+
135
+
136
+ # Generate 3D mesh
137
+ theta, r, phi = np.meshgrid(self.sim.domains.theta, self.sim.domains.r, self.sim.domains.phi, indexing='ij')
138
+ x, y, z = r * np.sin(theta) * np.cos(phi), r * np.sin(theta) * np.sin(phi), r * np.cos(theta)
139
+
140
+
141
+ # Create an empty DataFrame for the current field
142
+ columns = ["snapshot", "time", "x", "y", "z"]
143
+ if field == "gasv":
144
+ columns.extend(["vx", "vy", "vz"])
145
+ else:
146
+ columns.append(field)
147
+
148
+ df_snapshots = pd.DataFrame(columns=columns)
149
+
150
+ for i, snap in enumerate(snaps):
151
+ row = {"snapshot": snap, "time": time_values[i], "x": x.ravel(), "y": y.ravel(), "z": z.ravel()}
152
+
153
+ if field == "gasdens":
154
+ gasd = self.sim.load_field("gasdens", snapshot=snap, type="scalar")
155
+ row[field] = gasd.data.ravel()
156
+
157
+ elif field == "gasv":
158
+ gasv = self.sim.load_field("gasv", snapshot=snap, type="vector")
159
+ gasvx, gasvy, gasvz = gasv.to_cartesian()
160
+ row["vx"] = gasvx.data.ravel()
161
+ row["vy"] = gasvy.data.ravel()
162
+ row["vz"] = gasvz.data.ravel()
163
+
164
+ elif field == "gasenergy":
165
+ gasenergy = self.sim.load_field("gasenergy", snapshot=snap, type="scalar")
166
+ row[field] = gasenergy.data.ravel()
167
+
168
+ # Append the row to the DataFrame
169
+ df_snapshots = pd.concat([df_snapshots, pd.DataFrame([row])], ignore_index=True)
170
+
171
+ self.df = df_snapshots
172
+ # Return the single DataFrame
173
+ return df_snapshots
174
+
175
+
176
+ def evaluate(self, time, var1, var2, var3=None):
177
+ """
178
+ Interpolates a field in 2D or 3D depending on the provided parameters.
179
+
180
+ Parameters:
181
+ time (float): Time at which to interpolate.
182
+ var1, var2 (numpy.ndarray or float): Spatial coordinates in 2D.
183
+ var3 (numpy.ndarray or float, optional): Additional coordinate for 3D. If None, 2D is assumed.
184
+
185
+ Returns:
186
+ numpy.ndarray or float: Interpolated field values at the given coordinates.
187
+ If velocity fields are present, returns a tuple (vx, vy, vz) or (vx, vy).
188
+ """
189
+ # Automatically determine the field to interpolate
190
+ if "gasdens" in self.df.columns:
191
+ field_name = "gasdens"
192
+ elif "gasenergy" in self.df.columns:
193
+ field_name = "gasenergy"
194
+ elif {"vx", "vy", "vz"}.issubset(self.df.columns): # 3D velocity
195
+ field_name = ["vx", "vy", "vz"]
196
+ elif {"vx", "vy"}.issubset(self.df.columns): # 2D velocity (vx, vy)
197
+ field_name = ["vx", "vy"]
198
+ elif {"vx", "vz"}.issubset(self.df.columns): # 2D velocity (vx, vz)
199
+ field_name = ["vx", "vz"]
200
+ elif {"vy", "vz"}.issubset(self.df.columns): # 2D velocity (vy, vz)
201
+ field_name = ["vy", "vz"]
202
+ else:
203
+ raise ValueError("No valid field found in the DataFrame for interpolation.")
204
+
205
+ # Sort the DataFrame by time
62
206
  df_sorted = self.df.sort_values("time")
63
207
  idx = df_sorted["time"].searchsorted(time) - 1
64
208
  if idx == -1:
@@ -70,49 +214,122 @@ class DataHandler:
70
214
  if factor < 0:
71
215
  factor = 0
72
216
 
73
- def interp(idx):
74
- coord1, coord2 = df_sorted.iloc[idx]["coord1"], df_sorted.iloc[idx]["coord2"]
75
- points = np.column_stack((coord1.ravel(), coord2.ravel()))
76
- data = df_sorted.iloc[idx][field_name].ravel()
77
- return griddata(points, data, (var1, var2), method='linear', fill_value=0.0)
78
-
79
- result = (1 - factor) * interp(idx) + factor * interp(idx_after)
80
- return result
217
+ # Check if the input is a single point or a mesh
218
+ is_scalar = np.isscalar(var1) and np.isscalar(var2) and (var3 is None or np.isscalar(var3))
219
+ if is_scalar:
220
+ result_shape = ()
221
+ else:
222
+ result_shape = var1.shape # Preserve the shape of the input mesh
223
+
224
+ def interp(idx, field):
225
+ if var3 is not None: # 3D interpolation
226
+ coord_x = np.array(df_sorted.iloc[idx]["x"])
227
+ coord_y = np.array(df_sorted.iloc[idx]["y"])
228
+ coord_z = np.array(df_sorted.iloc[idx]["z"])
229
+ points = np.column_stack((coord_x.ravel(), coord_y.ravel(), coord_z.ravel()))
230
+ data = np.array(df_sorted.iloc[idx][field]).ravel()
231
+ return griddata(points, data, (var1, var2, var3), method='nearest', fill_value=0.0)
232
+ else: # 2D interpolation
233
+ if 'x' in self.df.columns and 'y' in self.df.columns:
234
+ coord1 = np.array(df_sorted.iloc[idx]["x"])
235
+ coord2 = np.array(df_sorted.iloc[idx]["y"])
236
+ elif 'x' in self.df.columns and 'z' in self.df.columns:
237
+ coord1 = np.array(df_sorted.iloc[idx]["x"])
238
+ coord2 = np.array(df_sorted.iloc[idx]["z"])
239
+ elif 'y' in self.df.columns and 'z' in self.df.columns:
240
+ coord1 = np.array(df_sorted.iloc[idx]["y"])
241
+ coord2 = np.array(df_sorted.iloc[idx]["z"])
242
+ else:
243
+ raise ValueError("Insufficient spatial coordinates for interpolation.")
244
+ points = np.column_stack((coord1.ravel(), coord2.ravel()))
245
+ data = np.array(df_sorted.iloc[idx][field]).ravel()
246
+ return griddata(points, data, (var1, var2), method='linear', fill_value=0.0)
247
+
248
+ # Preallocate arrays for results
249
+ if isinstance(field_name, list): # Velocity (multiple fields)
250
+ results = []
251
+ for field in field_name:
252
+ interpolated = (1 - factor) * interp(idx, field) + factor * interp(idx_after, field)
253
+ if is_scalar:
254
+ results.append(interpolated.item()) # Extract scalar value
255
+ else:
256
+ results.append(interpolated)
257
+ return results
258
+ else: # Scalar field (gasdens or gasenergy)
259
+ interpolated = (1 - factor) * interp(idx, field_name) + factor * interp(idx_after, field_name)
260
+ if is_scalar:
261
+ return interpolated.item() # Extract scalar value
262
+ else:
263
+ return interpolated
81
264
 
82
- def interpolate_velocity(self, time, var1, var2):
83
- v1 = self.interpolate_field(time, var1, var2, "vel1")
84
- v2 = self.interpolate_field(time, var1, var2, "vel2")
85
- return v1, v2
86
265
 
87
- def interpolate_density(self, time, var1, var2):
88
- return self.interpolate_field(time, var1, var2, "gasdens")
89
266
 
90
267
 
91
- class Simulation:
268
+ class fluidmotion:
92
269
  def __init__(self, plane, angle, num_snapshots, dir_path):
270
+ """
271
+ Initializes the Simulation class.
272
+
273
+ Parameters:
274
+ plane (str): The plane for 2D data ('XZ', 'XY', 'YZ').
275
+ angle (float): The angle for the 2D slice.
276
+ num_snapshots (int): Number of snapshots for 2D data.
277
+ dir_path (str): Directory path where the simulation data is stored.
278
+ """
93
279
  self.sim = fp.Simulation(output_dir=dir_path)
94
280
  self.data_handler = DataHandler(self.sim)
95
281
  self.data_handler.load_data(plane, angle, num_snapshots)
96
282
 
97
283
  def velocity_field(self, t, y):
284
+ """
285
+ Computes the velocity field at a given time and position.
286
+
287
+ Parameters:
288
+ t (float): Time at which to compute the velocity field.
289
+ y (array-like): Position [var1, var2] where the velocity is computed.
290
+
291
+ Returns:
292
+ numpy.ndarray: Velocity vector [v1, v2] at the given position and time.
293
+ """
98
294
  var1, var2 = y
99
295
  v1, v2 = self.data_handler.interpolate_velocity(t, np.array([var1]), np.array([var2]))
100
296
  return np.array([v1[0], v2[0]])
101
297
 
102
298
  def integrate_particles(self, particle_pos, time, dt=0.01):
103
- """ Integra todas las partículas con un paso explícito de Euler."""
299
+ """
300
+ Integrates all particles using an explicit Euler step.
301
+
302
+ Parameters:
303
+ particle_pos (numpy.ndarray): Array of particle positions (shape: [n_particles, 2]).
304
+ time (float): Current time of the simulation.
305
+ dt (float): Time step for integration.
306
+
307
+ Returns:
308
+ numpy.ndarray: Updated particle positions after integration.
309
+ """
104
310
  if len(particle_pos) == 0:
105
311
  return np.array([])
106
312
 
107
313
  v1, v2 = self.data_handler.interpolate_velocity(time, particle_pos[:, 0], particle_pos[:, 1])
108
314
 
109
- # Paso de Euler: x_{n+1} = x_n + v * dt
315
+ # Euler step: x_{n+1} = x_n + v * dt
110
316
  particle_pos[:, 0] += v1 * dt
111
317
  particle_pos[:, 1] += v2 * dt
112
318
 
113
319
  return particle_pos
114
320
 
115
321
  def generate_uniform_particles(self, var1_min, var1_max, var2_min, var2_max, num_particles):
322
+ """
323
+ Generates uniformly distributed particles within a specified region.
324
+
325
+ Parameters:
326
+ var1_min, var1_max (float): Range for the first coordinate.
327
+ var2_min, var2_max (float): Range for the second coordinate.
328
+ num_particles (int): Number of particles to generate.
329
+
330
+ Returns:
331
+ numpy.ndarray: Array of particle positions (shape: [num_particles, 2]).
332
+ """
116
333
  grid_size = int(np.sqrt(num_particles))
117
334
  var1_candidates = np.linspace(var1_min + 0.01, var1_max - 0.01, grid_size)
118
335
  var2_candidates = np.linspace(var2_min + 0.001, var2_max - 0.001, grid_size)
@@ -133,6 +350,21 @@ class Simulation:
133
350
  return new_particles
134
351
 
135
352
  def run_simulation(self, res, var1_min, var1_max, var2_min, var2_max, ts, npi, max_lifetime, generation_interval):
353
+ """
354
+ Runs the particle simulation and generates an animation.
355
+
356
+ Parameters:
357
+ res (int): Resolution of the grid for density interpolation.
358
+ var1_min, var1_max (float): Range for the first coordinate.
359
+ var2_min, var2_max (float): Range for the second coordinate.
360
+ ts (int): Number of time steps for the simulation.
361
+ npi (int): Number of particles to generate at each interval.
362
+ max_lifetime (int): Maximum lifetime of particles.
363
+ generation_interval (int): Interval for generating new particles.
364
+
365
+ Returns:
366
+ IPython.display.Video: Animation of the particle simulation.
367
+ """
136
368
  var1_reg, var2_reg = np.linspace(var1_min, var1_max, res), np.linspace(var2_min, var2_max, res)
137
369
  VAR1_reg, VAR2_reg = np.meshgrid(var1_reg, var2_reg, indexing='ij')
138
370
 
@@ -143,14 +375,14 @@ class Simulation:
143
375
  lifetimes = np.empty(0)
144
376
  new_particles = self.generate_uniform_particles(var1_min, var1_max, var2_min, var2_max, npi)
145
377
 
146
- # Determinar el label del eje y en función del plano
378
+ # Determine the y-axis label based on the plane
147
379
  plane = self.data_handler.plane
148
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
380
+ y_label = "Z [AU]" if plane == "XZ" else "Y [AU]"
149
381
 
150
382
  fig, ax = plt.subplots(figsize=(8, 8))
151
383
  camera = Camera(fig)
152
384
 
153
- with tqdm(total=len(t_eval), desc="Generando animación", unit="frame") as pbar:
385
+ with tqdm(total=len(t_eval), desc="Generating animation", unit="frame") as pbar:
154
386
  for frame in range(len(t_eval)):
155
387
  time = t_eval[frame]
156
388
  if frame % generation_interval == 0:
@@ -167,437 +399,31 @@ class Simulation:
167
399
 
168
400
  lifetimes_normalized = lifetimes / max_lifetime
169
401
 
170
- # Agregar interpolación de densidad como fondo
402
+ # Add density interpolation as background
171
403
  gasd_interpolated = self.data_handler.interpolate_density(time, VAR1_reg, VAR2_reg)
172
404
  c = ax.pcolormesh(VAR1_reg, VAR2_reg, np.log10(gasd_interpolated * self.sim.URHO * 1e3),
173
- cmap="viridis", shading='auto')
405
+ cmap="viridis", shading='auto')
174
406
 
175
- # Graficar partículas
407
+ # Plot particles
176
408
  if len(particle_pos) > 0:
177
409
  ax.scatter(particle_pos[:, 0], particle_pos[:, 1], c='lightgray', alpha=lifetimes_normalized, s=1.0)
178
410
 
179
411
  ax.set_xlim(var1_min, var1_max)
180
412
  ax.set_ylim(var2_min, var2_max)
181
- ax.set_xlabel(r"$r \ [AU]$",size=12)
182
- ax.set_ylabel(y_label,size=12)
413
+ ax.set_xlabel(r"$r \ [AU]$", size=12)
414
+ ax.set_ylabel(y_label, size=12)
183
415
  camera.snap()
184
416
 
185
417
  pbar.update(1)
186
418
 
187
- # Agregar barra de color
419
+ # Add color bar
188
420
  fig.colorbar(c, ax=ax, label=r'$\log_{10}(\rho)$ [kg/m$^3$]')
189
421
  plt.close(fig)
190
422
  animation = camera.animate()
191
- video_filename = 'figures/streaklines.mp4'
423
+ video_filename = 'particles.mp4'
192
424
  animation.save(video_filename, writer=FFMpegWriter(fps=10, codec='libx264', bitrate=5000))
193
425
 
194
- # Mostrar el video en el entorno interactivo
426
+ # Display the video in the interactive environment
195
427
  return Video(video_filename, embed=True)
196
428
 
197
429
 
198
- class Visualize:
199
- def __init__(self, data_handler):
200
- """
201
- Inicializa la clase Visualize con una instancia de DataHandler.
202
- """
203
- self.data_handler = data_handler
204
-
205
- def density(self, var1_min, var1_max, var2_min, var2_max, res, time):
206
- """
207
- Grafica un mapa de contornos de densidad interpolada en un tiempo dado.
208
- """
209
- # Crear una cuadrícula regular para las coordenadas
210
- var1 = np.linspace(var1_min, var1_max, res)
211
- var2 = np.linspace(var2_min, var2_max, res)
212
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
213
-
214
- # Interpolar la densidad en el tiempo dado
215
- density = self.data_handler.interpolate_density(time, VAR1, VAR2).T
216
-
217
- # Determinar el label del eje y en función del plano
218
- plane = self.data_handler.plane
219
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
220
-
221
- # Crear el gráfico de contorno
222
- fig = go.Figure(
223
- data=go.Contour(
224
- z=np.log10(density * self.data_handler.sim.URHO * 1e3),
225
- x=var1,
226
- y=var2,
227
- colorscale="Spectral_r",
228
- contours=dict(coloring="heatmap"),
229
- colorbar=dict(title="log10(ρ) [kg/m³]")
230
- )
231
- )
232
-
233
- # Configurar el diseño del gráfico
234
- fig.update_layout(
235
- title=f"Mapa de Contornos de Densidad (t = {time:.2f})",
236
- xaxis_title="r [AU]",
237
- yaxis_title=y_label,
238
- width=600,
239
- height=600
240
- )
241
-
242
- # Mostrar el gráfico
243
- fig.show()
244
-
245
-
246
-
247
-
248
- def animate_density(self, var1_min, var1_max, var2_min, var2_max, res, time_array):
249
-
250
- # Crear una cuadrícula regular para las coordenadas
251
- var1 = np.linspace(var1_min, var1_max, res)
252
- var2 = np.linspace(var2_min, var2_max, res)
253
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
254
-
255
- # Precalcular el campo de densidad para todos los tiempos
256
- precalculated_density = [
257
- self.data_handler.interpolate_density(time, VAR1, VAR2).T
258
- for time in time_array
259
- ]
260
- # Determinar el label del eje y en función del plano
261
- plane = self.data_handler.plane
262
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
263
-
264
- # Crear la figura inicial
265
- initial_density = precalculated_density[0]
266
- fig = go.Figure(
267
- data=go.Contour(
268
- z=np.log10(initial_density * self.data_handler.sim.URHO * 1e3),
269
- x=var1,
270
- y=var2,
271
- colorscale="Spectral_r",
272
- contours=dict(coloring="heatmap"),
273
- colorbar=dict(title="log10(ρ) [kg/m³]")
274
- )
275
- )
276
-
277
- # Crear los frames para la animación
278
- frames = []
279
- for i, time in enumerate(time_array):
280
- density = precalculated_density[i]
281
- frames.append(go.Frame(
282
- data=go.Contour(
283
- z=np.log10(density * self.data_handler.sim.URHO * 1e3),
284
- x=var1,
285
- y=var2,
286
- colorscale="Spectral_r", # Este colormap será actualizado dinámicamente
287
- contours=dict(coloring="heatmap")
288
- ),
289
- name=f"{time:.2f}"
290
- ))
291
-
292
- fig.frames = frames
293
-
294
- # Configurar el slider
295
- sliders = [
296
- dict(
297
- steps=[
298
- dict(
299
- method="animate",
300
- args=[[f"{time:.2f}"], dict(mode="immediate", frame=dict(duration=100, redraw=True), transition=dict(duration=0))],
301
- label=f"{time:.2f}"
302
- )
303
- for time in time_array
304
- ],
305
- transition=dict(duration=0),
306
- currentvalue=dict(font=dict(size=16), prefix="Tiempo: ", visible=True),
307
- len=0.9
308
- )
309
- ]
310
-
311
- # Configurar los botones de reproducción y pausa
312
- updatemenus = [
313
- dict(
314
- type="buttons",
315
- showactive=False,
316
- buttons=[
317
- dict(label="Play",
318
- method="animate",
319
- args=[None, dict(frame=dict(duration=100, redraw=True), fromcurrent=True)]),
320
- dict(label="Pause",
321
- method="animate",
322
- args=[[None], dict(frame=dict(duration=0, redraw=False), mode="immediate")])
323
- ]
324
- )
325
- ]
326
-
327
- # Actualizar el diseño de la figura
328
- fig.update_layout(
329
- title="Evolución del Campo de Densidad",
330
- xaxis_title='r [AU]',
331
- yaxis_title=y_label,
332
- width=600,
333
- height=600,
334
- sliders=sliders,
335
- updatemenus=updatemenus
336
- )
337
-
338
- # Mostrar la animación
339
- fig.show()
340
-
341
- def velocity(self, var1_min, var1_max, var2_min, var2_max, res, time):
342
- """
343
- Grafica un mapa de magnitud de velocidad interpolada en un tiempo dado.
344
- Las zonas donde la interpolación da 0 no serán coloreadas.
345
- """
346
- # Crear una cuadrícula regular para las coordenadas
347
- var1 = np.linspace(var1_min, var1_max, res)
348
- var2 = np.linspace(var2_min, var2_max, res)
349
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
350
-
351
- # Interpolar el campo de velocidad en el tiempo dado
352
- velocity_x, velocity_y = self.data_handler.interpolate_velocity(time, VAR1, VAR2)
353
-
354
- # Calcular la magnitud de la velocidad
355
- velocity_magnitude = np.sqrt(velocity_x**2 + velocity_y**2).T * self.data_handler.sim.UV / (1e5) # Convertir a km/s
356
-
357
- # Establecer las zonas donde velocity_x y velocity_y son 0 como None
358
- velocity_magnitude[velocity_magnitude==0] = None
359
-
360
- # Determinar el label del eje y en función del plano
361
- plane = self.data_handler.plane
362
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
363
-
364
- # Crear el gráfico de contorno
365
- fig = go.Figure(
366
- data=go.Contour(
367
- z=velocity_magnitude,
368
- x=var1,
369
- y=var2,
370
- colorscale="Viridis",
371
- contours=dict(coloring="heatmap"),
372
- colorbar=dict(title="|v| [km/s]"),
373
- zmin=np.nanmin(velocity_magnitude),
374
- zmax=np.nanmax(velocity_magnitude),
375
- showscale=True
376
- )
377
- )
378
-
379
- # Configurar el diseño del gráfico
380
- fig.update_layout(
381
- title=f"Mapa de Velocidad (t = {time:.2f})",
382
- xaxis_title="r [AU]",
383
- yaxis_title=y_label,
384
- width=600,
385
- height=600,
386
- plot_bgcolor="white" # Fondo blanco para que las zonas no coloreadas sean visibles
387
- )
388
-
389
- # Mostrar el gráfico
390
- fig.show()
391
-
392
- def animate_velocity(self, var1_min, var1_max, var2_min, var2_max, res, time_array):
393
- """
394
- Crea una animación del mapa de magnitud de velocidad interpolada en función del tiempo.
395
- Las zonas donde la magnitud de la velocidad es 0 no serán coloreadas.
396
- """
397
- # Crear una cuadrícula regular para las coordenadas
398
- var1 = np.linspace(var1_min, var1_max, res)
399
- var2 = np.linspace(var2_min, var2_max, res)
400
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
401
-
402
- # Determinar el label del eje y en función del plano
403
- plane = self.data_handler.plane
404
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
405
-
406
- # Precalcular la magnitud del campo de velocidad para todos los tiempos
407
- precalculated_velocity_magnitude = []
408
- for time in time_array:
409
- velocity_x, velocity_y = self.data_handler.interpolate_velocity(time, VAR1, VAR2)
410
- velocity_magnitude = np.sqrt(velocity_x**2 + velocity_y**2).T * self.data_handler.sim.UV / (1e5) # Convertir a km/s
411
- velocity_magnitude[velocity_magnitude == 0] = None # Reemplazar las zonas con magnitud 0 por None
412
- precalculated_velocity_magnitude.append(velocity_magnitude)
413
-
414
- # Crear la figura inicial
415
- initial_velocity_magnitude = precalculated_velocity_magnitude[0]
416
- fig = go.Figure(
417
- data=go.Contour(
418
- z=initial_velocity_magnitude,
419
- x=var1,
420
- y=var2,
421
- colorscale="Viridis",
422
- contours=dict(coloring="heatmap"),
423
- colorbar=dict(title="|v| [km/s]")
424
- )
425
- )
426
-
427
- # Crear los frames para la animación
428
- frames = []
429
- for i, time in enumerate(time_array):
430
- velocity_magnitude = precalculated_velocity_magnitude[i]
431
- frames.append(go.Frame(
432
- data=go.Contour(
433
- z=velocity_magnitude,
434
- x=var1,
435
- y=var2,
436
- colorscale="Viridis",
437
- contours=dict(coloring="heatmap")
438
- ),
439
- name=f"{time:.2f}"
440
- ))
441
-
442
- fig.frames = frames
443
-
444
- # Configurar el slider
445
- sliders = [
446
- dict(
447
- steps=[
448
- dict(
449
- method="animate",
450
- args=[[f"{time:.2f}"], dict(mode="immediate", frame=dict(duration=150, redraw=True), transition=dict(duration=0))],
451
- label=f"{time:.2f}"
452
- )
453
- for time in time_array
454
- ],
455
- transition=dict(duration=0),
456
- currentvalue=dict(font=dict(size=16), prefix="Tiempo: ", visible=True),
457
- len=0.9
458
- )
459
- ]
460
-
461
- # Configurar los botones de reproducción y pausa
462
- updatemenus = [
463
- dict(
464
- type="buttons",
465
- showactive=False,
466
- buttons=[
467
- dict(label="Play",
468
- method="animate",
469
- args=[None, dict(frame=dict(duration=100, redraw=True), fromcurrent=True)]),
470
- dict(label="Pause",
471
- method="animate",
472
- args=[[None], dict(frame=dict(duration=0, redraw=False), mode="immediate")])
473
- ]
474
- )
475
- ]
476
-
477
- # Actualizar el diseño de la figura
478
- fig.update_layout(
479
- title="Evolución del Campo de Velocidad",
480
- xaxis_title='r [AU]',
481
- yaxis_title=y_label,
482
- width=600,
483
- height=600,
484
- sliders=sliders,
485
- updatemenus=updatemenus
486
- )
487
-
488
- # Mostrar la animación
489
- fig.show()
490
-
491
- def vel_streamlines(self, var1_min, var1_max, var2_min, var2_max, res, time):
492
- """
493
- Grafica las streamlines del campo de velocidad con la densidad de fondo para un tiempo dado.
494
- """
495
- # Crear una cuadrícula regular para las coordenadas
496
- var1 = np.linspace(var1_min, var1_max, res)
497
- var2 = np.linspace(var2_min, var2_max, res)
498
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
499
-
500
- # Interpolar el campo de densidad y velocidad en el tiempo dado
501
- density = self.data_handler.interpolate_density(time, VAR1, VAR2)
502
- velocity_x, velocity_y = self.data_handler.interpolate_velocity(time, VAR1, VAR2)
503
-
504
- # Calcular la magnitud de la velocidad
505
- v_mag = np.sqrt(velocity_x**2 + velocity_y**2).T * self.data_handler.sim.UV / (1e5) # Convertir a km/s
506
-
507
- # Determinar el label del eje y en función del plano
508
- plane = self.data_handler.plane
509
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
510
-
511
- # Crear el gráfico
512
- plt.figure(figsize=(6, 6))
513
- plt.pcolormesh(var1, var2, np.log10(density * self.data_handler.sim.URHO * 1e3).T, cmap="Spectral_r", shading='auto')
514
- plt.streamplot(var1, var2, velocity_x.T, velocity_y.T, color=v_mag, linewidth=0.7, density=3.0, cmap='viridis')
515
- plt.colorbar(label="|v| [km/s]")
516
- plt.title(f"Streamlines (t = {time:.2f})")
517
- plt.xlabel("r [AU]")
518
- plt.ylabel(y_label)
519
- plt.show()
520
-
521
- def vel_streamlines_slide(self, var1_min, var1_max, var2_min, var2_max, res, time_array):
522
-
523
- # Crear una cuadrícula regular para las coordenadas
524
- var1 = np.linspace(var1_min, var1_max, res)
525
- var2 = np.linspace(var2_min, var2_max, res)
526
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
527
- # Determinar el label del eje y en función del plano
528
- plane = self.data_handler.plane
529
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
530
-
531
- # Precalcular densidad y velocidades para todos los tiempos
532
- precalculated_data = [
533
- {
534
- "density": self.data_handler.interpolate_density(time, VAR1, VAR2),
535
- "velocity": self.data_handler.interpolate_velocity(time, VAR1, VAR2)
536
- }
537
- for time in time_array
538
- ]
539
-
540
- def update_plot(time_index):
541
- """
542
- Actualiza el gráfico para un índice de tiempo dado.
543
- """
544
- data = precalculated_data[time_index]
545
- density = data["density"]
546
- velocity_x = data["velocity"][0]
547
- velocity_y = data["velocity"][1]
548
- v_mag = np.sqrt(velocity_x**2 + velocity_y**2).T*self.data_handler.sim.UV/(1e5)
549
- # Crear el gráfico
550
- plt.figure(figsize=(6, 6))
551
- plt.pcolormesh(var1, var2, np.log10(density * self.data_handler.sim.URHO * 1e3).T, cmap="Spectral_r", shading='auto')
552
- plt.streamplot(var1, var2, velocity_x.T, velocity_y.T, color=v_mag, linewidth=0.7, density=3.0,cmap='viridis')
553
- plt.colorbar(label="|v| [km/s]")
554
- plt.title(f"Streamlines (t = {time_array[time_index]:.2f})")
555
- plt.xlabel("r [AU]")
556
- plt.ylabel(y_label)
557
- plt.show()
558
-
559
- # Crear un slider interactivo para el tiempo (con índices enteros)
560
- interact(update_plot, time_index=IntSlider(value=0, min=0, max=len(time_array) - 1, step=1))
561
-
562
-
563
- def vel_streamlines_vid(self, var1_min, var1_max, var2_min, var2_max, res, time_array,output_file="streamlines_animation.mp4"):
564
-
565
- # Crear una cuadrícula regular para las coordenadas
566
- var1 = np.linspace(var1_min, var1_max, res)
567
- var2 = np.linspace(var2_min, var2_max, res)
568
- VAR1, VAR2 = np.meshgrid(var1, var2, indexing='ij')
569
- # Determinar el label del eje y en función del plano
570
- plane = self.data_handler.plane
571
- y_label = "Z [AU]" if plane == "XZ" else r"$\phi$ [rad]"
572
-
573
- # Configurar la figura y la cámara para la animación
574
- fig, ax = plt.subplots(figsize=(6, 6))
575
- camera = Camera(fig)
576
-
577
- # Generar los frames de la animación
578
- for time in time_array:
579
- # Interpolar el campo de densidad y velocidad
580
- density = self.data_handler.interpolate_density(time, VAR1, VAR2)
581
- velocity_x, velocity_y = self.data_handler.interpolate_velocity(time, VAR1, VAR2)
582
- v_mag = np.sqrt(velocity_x**2 + velocity_y**2).T*self.data_handler.sim.UV/(1e5)
583
-
584
- # Crear el gráfico para el frame actual
585
- c = ax.pcolormesh(var1, var2, np.log10(density * self.data_handler.sim.URHO * 1e3).T, cmap="Spectral_r", shading='auto')
586
- strm=ax.streamplot(var1, var2, velocity_x.T, velocity_y.T, color=v_mag, linewidth=0.7, density=3.0,cmap='viridis')
587
- ax.set_xlabel("r [AU]")
588
- ax.set_ylabel(y_label)
589
- camera.snap()
590
-
591
-
592
- # Crear la animación
593
- fig.colorbar(strm.lines, ax=ax, label="|v| [km/s]")
594
- plt.close(fig)
595
- animation = camera.animate()
596
-
597
-
598
- # Guardar la animación en un archivo
599
- animation.save(output_file, writer="ffmpeg", fps=10)
600
-
601
- plt.close(fig)
602
- return Video(output_file, embed=True)
603
-