fargopy 0.3.15__py3-none-any.whl → 1.0.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/__init__.py +9 -346
- fargopy/base.py +377 -0
- fargopy/bin/ifargopy +91 -0
- fargopy/bin/vfargopy +2111 -0
- fargopy/data/fargopy_logo.png +0 -0
- fargopy/fields.py +1590 -44
- fargopy/flux.py +894 -0
- fargopy/plot.py +553 -8
- fargopy/simulation.py +1597 -438
- fargopy/sys.py +116 -65
- fargopy/tests/test_base.py +8 -0
- fargopy/tests/test_flux.py +76 -0
- fargopy/tests/test_interp.py +132 -0
- fargopy-1.0.0.data/scripts/ifargopy +91 -0
- fargopy-1.0.0.data/scripts/vfargopy +2111 -0
- fargopy-1.0.0.dist-info/METADATA +425 -0
- fargopy-1.0.0.dist-info/RECORD +21 -0
- {fargopy-0.3.15.dist-info → fargopy-1.0.0.dist-info}/WHEEL +1 -1
- fargopy-1.0.0.dist-info/licenses/LICENSE +661 -0
- fargopy/fsimulation.py +0 -603
- fargopy/tests/test___init__.py +0 -0
- fargopy/util.py +0 -21
- fargopy/version.py +0 -1
- fargopy-0.3.15.data/scripts/ifargopy +0 -15
- fargopy-0.3.15.dist-info/METADATA +0 -489
- fargopy-0.3.15.dist-info/RECORD +0 -16
- fargopy-0.3.15.dist-info/licenses/LICENSE +0 -21
- {fargopy-0.3.15.dist-info → fargopy-1.0.0.dist-info}/entry_points.txt +0 -0
- {fargopy-0.3.15.dist-info → fargopy-1.0.0.dist-info}/top_level.txt +0 -0
fargopy/plot.py
CHANGED
|
@@ -7,6 +7,7 @@ import fargopy
|
|
|
7
7
|
# Required packages
|
|
8
8
|
###############################################################
|
|
9
9
|
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
10
11
|
|
|
11
12
|
###############################################################
|
|
12
13
|
# Constants
|
|
@@ -16,28 +17,50 @@ import matplotlib.pyplot as plt
|
|
|
16
17
|
# Classes
|
|
17
18
|
###############################################################
|
|
18
19
|
class Plot(object):
|
|
20
|
+
"""Plotting utilities and visualization helpers for FARGO3D data.
|
|
21
|
+
|
|
22
|
+
The ``Plot`` class encapsulates static methods for common plotting tasks,
|
|
23
|
+
such as adding watermarks to figures and creating standardized heatmaps
|
|
24
|
+
for simulation fields.
|
|
25
|
+
"""
|
|
19
26
|
|
|
20
27
|
@staticmethod
|
|
21
28
|
def fargopy_mark(ax):
|
|
22
|
-
"""Add a
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
"""Add a watermark to a 2D or 3D plot.
|
|
30
|
+
|
|
31
|
+
Places a rotated "FARGOpy {version}" watermark in the top-right corner
|
|
32
|
+
of the specified axes.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
ax : matplotlib.axes.Axes
|
|
37
|
+
The axes object where the watermark will be added.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
matplotlib.text.Text
|
|
42
|
+
The created text object.
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
Add watermark to a plot:
|
|
25
47
|
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
>>> fig, ax = plt.subplots()
|
|
49
|
+
>>> ax.plot([1, 2, 3], [1, 2, 3])
|
|
50
|
+
>>> fp.Plot.fargopy_mark(ax)
|
|
28
51
|
"""
|
|
29
52
|
#Get the height of axe
|
|
30
53
|
axh=ax.get_window_extent().transformed(ax.get_figure().dpi_scale_trans.inverted()).height
|
|
31
|
-
fig_factor=axh/
|
|
54
|
+
fig_factor=axh/8
|
|
32
55
|
|
|
33
56
|
#Options of the water mark
|
|
34
57
|
args=dict(
|
|
35
58
|
rotation=270,ha='left',va='top',
|
|
36
|
-
transform=ax.transAxes,color='pink',fontsize=
|
|
59
|
+
transform=ax.transAxes,color='pink',fontsize=10*fig_factor,zorder=100
|
|
37
60
|
)
|
|
38
61
|
|
|
39
62
|
#Text of the water mark
|
|
40
|
-
mark=f"FARGOpy {fargopy.
|
|
63
|
+
mark=f"FARGOpy {fargopy.__version__}"
|
|
41
64
|
|
|
42
65
|
#Choose the according to the fact it is a 2d or 3d plot
|
|
43
66
|
try:
|
|
@@ -48,3 +71,525 @@ class Plot(object):
|
|
|
48
71
|
|
|
49
72
|
text=plt_text(1,1,mark,**args);
|
|
50
73
|
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def plot_heatmap(data, x=None, y=None, title="Heatmap", xlabel="X", ylabel="Y", contour_levels=10):
|
|
78
|
+
"""Plot a 2D heatmap with pcolormesh and contours.
|
|
79
|
+
|
|
80
|
+
Creates a figure displaying the provided 2D data as a heatmap using a
|
|
81
|
+
reversed Spectral colormap, overlaid with black contour lines.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
data : np.ndarray
|
|
86
|
+
2D array of data to plot.
|
|
87
|
+
x : np.ndarray, optional
|
|
88
|
+
1D array of X-axis coordinates.
|
|
89
|
+
y : np.ndarray, optional
|
|
90
|
+
1D array of Y-axis coordinates.
|
|
91
|
+
title : str, optional
|
|
92
|
+
Plot title (default: "Heatmap").
|
|
93
|
+
xlabel : str, optional
|
|
94
|
+
X-axis label (default: "X").
|
|
95
|
+
ylabel : str, optional
|
|
96
|
+
Y-axis label (default: "Y").
|
|
97
|
+
contour_levels : int or list, optional
|
|
98
|
+
Number of contour levels or specific level values (default: 10).
|
|
99
|
+
|
|
100
|
+
Examples
|
|
101
|
+
--------
|
|
102
|
+
Plot a random heatmap:
|
|
103
|
+
|
|
104
|
+
>>> data = np.random.rand(10, 10)
|
|
105
|
+
>>> fp.Plot.plot_heatmap(data, title="Random Field")
|
|
106
|
+
"""
|
|
107
|
+
plt.figure(figsize=(8, 6))
|
|
108
|
+
|
|
109
|
+
if x is not None and y is not None:
|
|
110
|
+
extent = [x.min(), x.max(), y.min(), y.max()]
|
|
111
|
+
X, Y = np.meshgrid(x, y)
|
|
112
|
+
# Plot the heatmap with pcolormesh
|
|
113
|
+
mesh = plt.pcolormesh(X, Y, data, shading='auto', cmap='Spectral_r')
|
|
114
|
+
# Add contour lines
|
|
115
|
+
contours = plt.contour(X, Y, data, levels=contour_levels, colors='black', linewidths=0.5)
|
|
116
|
+
plt.clabel(contours, inline=True, fontsize=8, fmt="%.1f")
|
|
117
|
+
else:
|
|
118
|
+
# Plot the heatmap with pcolormesh
|
|
119
|
+
mesh = plt.pcolormesh(data, shading='auto', cmap='Spectral_r')
|
|
120
|
+
# Add contour lines
|
|
121
|
+
contours = plt.contour(data, levels=contour_levels, colors='black', linewidths=0.5)
|
|
122
|
+
plt.clabel(contours, inline=True, fontsize=8, fmt="%.1f")
|
|
123
|
+
|
|
124
|
+
plt.colorbar(mesh, label="Value")
|
|
125
|
+
plt.title(title)
|
|
126
|
+
plt.xlabel(xlabel)
|
|
127
|
+
plt.ylabel(ylabel)
|
|
128
|
+
plt.show()
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def interactive(sim):
|
|
132
|
+
"""
|
|
133
|
+
Interactive plot for the simulation using ipywidgets.
|
|
134
|
+
|
|
135
|
+
Allows selection of density, energy, or velocity (and component) for the colormap.
|
|
136
|
+
Provides controls for slice, resolution, interpolation, streamlines, and Hill radius overlay.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
sim : Simulation
|
|
141
|
+
The simulation object to interact with.
|
|
142
|
+
|
|
143
|
+
Examples
|
|
144
|
+
--------
|
|
145
|
+
>>> fp.Plot.interactive(sim)
|
|
146
|
+
"""
|
|
147
|
+
import ipywidgets as widgets
|
|
148
|
+
import matplotlib.pyplot as plt
|
|
149
|
+
from IPython.display import display, clear_output
|
|
150
|
+
|
|
151
|
+
# --- Widgets ---
|
|
152
|
+
time_slider = widgets.IntSlider(min=0, max=sim._get_nsnaps()-1, step=1, value=1, description='Snapshot')
|
|
153
|
+
slice_text = widgets.Text(value='theta=1.568', description='Slice')
|
|
154
|
+
res_slider = widgets.IntSlider(min=50, max=1000, step=10, value=500, description='Res')
|
|
155
|
+
interp_toggle = widgets.ToggleButton(value=False, description='Interpolate', icon='check')
|
|
156
|
+
progress = widgets.Label(value='')
|
|
157
|
+
streamlines_toggle = widgets.ToggleButton(value=False, description='Streamlines', icon='random')
|
|
158
|
+
density_slider = widgets.FloatSlider(min=1, max=10, step=0.5, value=3, description='Stream density')
|
|
159
|
+
hill_frac_slider = widgets.FloatSlider(min=0.1, max=2.0, step=0.05, value=1.0, description='Hill frac')
|
|
160
|
+
show_circle_toggle = widgets.ToggleButton(value=False, description='Show Hill', icon='circle')
|
|
161
|
+
cmap_options = ['Spectral_r', 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 'YlGnBu', 'cubehelix', 'twilight', 'turbo']
|
|
162
|
+
cmap_dropdown = widgets.Dropdown(options=cmap_options, value='Spectral_r', description='Colormap')
|
|
163
|
+
update_button = widgets.Button(description="Update", icon="refresh")
|
|
164
|
+
map_options = ['Densidad', 'Energia', 'Velocidad']
|
|
165
|
+
map_dropdown = widgets.Dropdown(options=map_options, value='Densidad', description='Mapa')
|
|
166
|
+
vel_components = ['vx', 'vy', 'vz']
|
|
167
|
+
vel_dropdown = widgets.Dropdown(options=vel_components, value='vx', description='Componente v')
|
|
168
|
+
vel_dropdown.layout.display = 'none' # Ocultar por defecto
|
|
169
|
+
|
|
170
|
+
def is_fixed(var, slice_str):
|
|
171
|
+
import re
|
|
172
|
+
match = re.search(rf'{var}=([^\[\],]+)', slice_str.replace(' ', ''))
|
|
173
|
+
return match is not None
|
|
174
|
+
|
|
175
|
+
# show or hide velocity component dropdown based on map selection
|
|
176
|
+
def on_map_change(change):
|
|
177
|
+
if change['new'] == 'Velocidad':
|
|
178
|
+
vel_dropdown.layout.display = ''
|
|
179
|
+
else:
|
|
180
|
+
vel_dropdown.layout.display = 'none'
|
|
181
|
+
map_dropdown.observe(on_map_change, names='value')
|
|
182
|
+
|
|
183
|
+
def plot_density(change=None):
|
|
184
|
+
clear_output(wait=True)
|
|
185
|
+
display(
|
|
186
|
+
time_slider, slice_text, res_slider, interp_toggle, streamlines_toggle,
|
|
187
|
+
density_slider, hill_frac_slider, show_circle_toggle, cmap_dropdown, map_dropdown, vel_dropdown, progress, update_button
|
|
188
|
+
)
|
|
189
|
+
import numpy as np
|
|
190
|
+
import re
|
|
191
|
+
|
|
192
|
+
slice_str = slice_text.value
|
|
193
|
+
res = res_slider.value
|
|
194
|
+
interpolate = interp_toggle.value
|
|
195
|
+
show_streamlines = streamlines_toggle.value
|
|
196
|
+
stream_density = density_slider.value
|
|
197
|
+
hill_frac = hill_frac_slider.value
|
|
198
|
+
show_circle = show_circle_toggle.value
|
|
199
|
+
cmap = cmap_dropdown.value
|
|
200
|
+
map_type = map_dropdown.value
|
|
201
|
+
vel_comp = vel_dropdown.value
|
|
202
|
+
|
|
203
|
+
# --- Ejes y nombres de malla ---
|
|
204
|
+
if is_fixed('theta', slice_str):
|
|
205
|
+
xlabel, ylabel = 'X', 'Y'
|
|
206
|
+
mesh_x_name = 'var1_mesh'
|
|
207
|
+
mesh_y_name = 'var2_mesh'
|
|
208
|
+
elif is_fixed('phi', slice_str):
|
|
209
|
+
xlabel, ylabel = 'X', 'Z'
|
|
210
|
+
mesh_x_name = 'var1_mesh'
|
|
211
|
+
mesh_y_name = 'var3_mesh'
|
|
212
|
+
else:
|
|
213
|
+
print("Warning: Please fix either theta or phi for a valid 2D slice (XY or XZ plane).")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
n = time_slider.value
|
|
217
|
+
|
|
218
|
+
if mesh_y_name == 'var2_mesh':
|
|
219
|
+
vel_dropdown.options = ['vx', 'vy']
|
|
220
|
+
if vel_dropdown.value not in vel_dropdown.options:
|
|
221
|
+
vel_dropdown.value = 'vx'
|
|
222
|
+
elif mesh_y_name == 'var3_mesh':
|
|
223
|
+
vel_dropdown.options = ['vx', 'vz']
|
|
224
|
+
if vel_dropdown.value not in vel_dropdown.options:
|
|
225
|
+
vel_dropdown.value = 'vx'
|
|
226
|
+
|
|
227
|
+
# --- Carga de datos según selección ---
|
|
228
|
+
if map_type == 'Densidad':
|
|
229
|
+
loader = sim.load_field(
|
|
230
|
+
fields=['gasdens', 'gasv'],
|
|
231
|
+
slice=slice_str,
|
|
232
|
+
snapshot=[n],
|
|
233
|
+
interpolate=interpolate
|
|
234
|
+
)
|
|
235
|
+
if interpolate:
|
|
236
|
+
gasdens = loader
|
|
237
|
+
gasv = loader
|
|
238
|
+
else:
|
|
239
|
+
gasdens, gasv = loader
|
|
240
|
+
elif map_type == 'Energia':
|
|
241
|
+
gasenergy_loader = sim.load_field(
|
|
242
|
+
fields='gasenergy',
|
|
243
|
+
slice=slice_str,
|
|
244
|
+
snapshot=[n],
|
|
245
|
+
interpolate=interpolate
|
|
246
|
+
)
|
|
247
|
+
if interpolate:
|
|
248
|
+
gasenergy = gasenergy_loader
|
|
249
|
+
else:
|
|
250
|
+
gasenergy = gasenergy_loader
|
|
251
|
+
gasv_loader = sim.load_field(
|
|
252
|
+
fields='gasv',
|
|
253
|
+
slice=slice_str,
|
|
254
|
+
snapshot=[n],
|
|
255
|
+
interpolate=interpolate
|
|
256
|
+
)
|
|
257
|
+
if interpolate:
|
|
258
|
+
gasv = gasv_loader
|
|
259
|
+
else:
|
|
260
|
+
gasv = gasv_loader
|
|
261
|
+
elif map_type == 'Velocidad':
|
|
262
|
+
gasv_loader = sim.load_field(
|
|
263
|
+
fields='gasv',
|
|
264
|
+
slice=slice_str,
|
|
265
|
+
snapshot=[n],
|
|
266
|
+
interpolate=interpolate
|
|
267
|
+
)
|
|
268
|
+
if interpolate:
|
|
269
|
+
gasv = gasv_loader
|
|
270
|
+
else:
|
|
271
|
+
gasv = gasv_loader
|
|
272
|
+
|
|
273
|
+
# --- Interpolación y selección de variable a graficar ---
|
|
274
|
+
if not interpolate:
|
|
275
|
+
if map_type == 'Densidad':
|
|
276
|
+
X = getattr(gasdens, mesh_x_name)[0]
|
|
277
|
+
Y = getattr(gasdens, mesh_y_name)[0]
|
|
278
|
+
data_map = np.log10(gasdens.gasdens_mesh[0] * sim.URHO)
|
|
279
|
+
elif map_type == 'Energia':
|
|
280
|
+
X = getattr(gasenergy, mesh_x_name)[0]
|
|
281
|
+
Y = getattr(gasenergy, mesh_y_name)[0]
|
|
282
|
+
data_map = np.log10(gasenergy.gasenergy_mesh[0])
|
|
283
|
+
elif map_type == 'Velocidad':
|
|
284
|
+
X = getattr(gasv, mesh_x_name)[0]
|
|
285
|
+
Y = getattr(gasv, mesh_y_name)[0]
|
|
286
|
+
idx = {'vx': 0, 'vy': 1, 'vz': 2}[vel_comp]
|
|
287
|
+
data_map = gasv.gasv_mesh[0][idx]
|
|
288
|
+
vx = vy = vmag = None
|
|
289
|
+
else:
|
|
290
|
+
progress.value = "Interpolando..."
|
|
291
|
+
if mesh_y_name == 'var2_mesh':
|
|
292
|
+
xmin, xmax = getattr(gasv, mesh_x_name)[0].min(), getattr(gasv, mesh_x_name)[0].max()
|
|
293
|
+
ymin, ymax = getattr(gasv, mesh_y_name)[0].min(), getattr(gasv, mesh_y_name)[0].max()
|
|
294
|
+
xs = np.linspace(xmin, xmax, res)
|
|
295
|
+
ys = np.linspace(ymin, ymax, res)
|
|
296
|
+
X, Y = np.meshgrid(xs, ys)
|
|
297
|
+
if map_type == 'Densidad':
|
|
298
|
+
data_map = gasdens.evaluate(time=n, var1=X, var2=Y, field='gasdens')
|
|
299
|
+
data_map = np.log10(data_map * sim.URHO)
|
|
300
|
+
vel = gasv.evaluate(time=n, var1=X, var2=Y, field='gasv')
|
|
301
|
+
vx = vel[0]
|
|
302
|
+
vy = vel[1]
|
|
303
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
304
|
+
elif map_type == 'Energia':
|
|
305
|
+
data_map = gasenergy.evaluate(time=n, var1=X, var2=Y, field='gasenergy')
|
|
306
|
+
#data_map = np.log10(data_map)
|
|
307
|
+
vel = gasv.evaluate(time=n, var1=X, var2=Y, field='gasv')
|
|
308
|
+
vx = vel[0]
|
|
309
|
+
vy = vel[1]
|
|
310
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
311
|
+
elif map_type == 'Velocidad':
|
|
312
|
+
vel = gasv.evaluate(time=n, var1=X, var2=Y, field='gasv')
|
|
313
|
+
idx = {'vx': 0, 'vy': 1, 'vz': 2}[vel_comp]
|
|
314
|
+
data_map = vel[idx]
|
|
315
|
+
vx = vel[0]
|
|
316
|
+
vy = vel[1]
|
|
317
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
318
|
+
else:
|
|
319
|
+
xmin, xmax = getattr(gasv, mesh_x_name)[0].min(), getattr(gasv, mesh_x_name)[0].max()
|
|
320
|
+
zmin, zmax = getattr(gasv, mesh_y_name)[0].min(), getattr(gasv, mesh_y_name)[0].max()
|
|
321
|
+
xs = np.linspace(xmin, xmax, res)
|
|
322
|
+
zs = np.linspace(zmin, zmax, res)
|
|
323
|
+
X, Y = np.meshgrid(xs, zs)
|
|
324
|
+
if map_type == 'Densidad':
|
|
325
|
+
data_map = gasdens.evaluate(time=n, var1=X, var3=Y, field='gasdens')
|
|
326
|
+
data_map = np.log10(data_map * sim.URHO)
|
|
327
|
+
vel = gasv.evaluate(time=n, var1=X, var3=Y, field='gasv')
|
|
328
|
+
vx = vel[0]
|
|
329
|
+
vy = vel[2]
|
|
330
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
331
|
+
elif map_type == 'Energia':
|
|
332
|
+
data_map = gasenergy.evaluate(time=n, var1=X, var3=Y, field='gasenergy')
|
|
333
|
+
#data_map = np.log10(data_map)
|
|
334
|
+
vel = gasv.evaluate(time=n, var1=X, var3=Y, field='gasv')
|
|
335
|
+
vx = vel[0]
|
|
336
|
+
vy = vel[2]
|
|
337
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
338
|
+
elif map_type == 'Velocidad':
|
|
339
|
+
vel = gasv.evaluate(time=n, var1=X, var3=Y, field='gasv')
|
|
340
|
+
|
|
341
|
+
idx = {'vx': 0, 'vy': 1, 'vz': 2}[vel_comp]
|
|
342
|
+
data_map = vel[idx]
|
|
343
|
+
vx = vel[0]
|
|
344
|
+
vy = vel[2]
|
|
345
|
+
vmag = np.sqrt(vx**2 + vy**2)
|
|
346
|
+
|
|
347
|
+
# --- Máscara por rango r (igual que antes) ---
|
|
348
|
+
r = np.sqrt(X**2 + Y**2)
|
|
349
|
+
r_match = re.search(r"r=\[([0-9\.]+),([0-9\.]+)\]", slice_str.replace(" ", ""))
|
|
350
|
+
if r_match:
|
|
351
|
+
r_min = float(r_match.group(1))
|
|
352
|
+
r_max = float(r_match.group(2))
|
|
353
|
+
else:
|
|
354
|
+
r_min = None
|
|
355
|
+
r_max = None
|
|
356
|
+
|
|
357
|
+
if r_min is not None and r_max is not None:
|
|
358
|
+
mask = (r >= r_min) & (r <= r_max)
|
|
359
|
+
data_map = np.where(mask, data_map, np.nan)
|
|
360
|
+
if show_streamlines and vx is not None and vy is not None and vmag is not None:
|
|
361
|
+
vx = np.where(mask, vx, np.nan)
|
|
362
|
+
vy = np.where(mask, vy, np.nan)
|
|
363
|
+
vmag = np.where(mask, vmag, np.nan)
|
|
364
|
+
|
|
365
|
+
# --- Plot ---
|
|
366
|
+
fig, ax = plt.subplots(figsize=(7,5))
|
|
367
|
+
pcm = ax.pcolormesh(X*sim.UL/sim.AU, Y*sim.UL/sim.AU, data_map, shading='auto', cmap=cmap)
|
|
368
|
+
|
|
369
|
+
# Mostrar streamlines para cualquier tipo de mapa si están disponibles
|
|
370
|
+
stream_obj = None
|
|
371
|
+
if interpolate and show_streamlines and vx is not None and vy is not None:
|
|
372
|
+
stream_obj = ax.streamplot(
|
|
373
|
+
X*sim.UL/sim.AU, Y*sim.UL/sim.AU, vx, vy,
|
|
374
|
+
color=vmag*sim.UL/sim.UT*1e-5 if vmag is not None else None,
|
|
375
|
+
linewidth=0.5,
|
|
376
|
+
density=stream_density,
|
|
377
|
+
cmap='viridis',
|
|
378
|
+
arrowsize=1
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# --- Hill radius
|
|
382
|
+
planets = sim.load_planets(snapshot=n)
|
|
383
|
+
if planets:
|
|
384
|
+
center_x = planets[0].pos.x
|
|
385
|
+
center_y = planets[0].pos.y
|
|
386
|
+
radius = hill_frac * planets[0].hill_radius
|
|
387
|
+
else:
|
|
388
|
+
center_x = 0
|
|
389
|
+
center_y = 0
|
|
390
|
+
radius = 0
|
|
391
|
+
|
|
392
|
+
if show_circle:
|
|
393
|
+
if is_fixed('theta', slice_str):
|
|
394
|
+
circle = plt.Circle((center_x*sim.UL/sim.AU, center_y*sim.UL/sim.AU), radius*sim.UL/sim.AU, color='black', fill=False, linestyle='--', linewidth=1)
|
|
395
|
+
ax.add_patch(circle)
|
|
396
|
+
elif is_fixed('phi', slice_str):
|
|
397
|
+
theta = np.linspace(0, np.pi, 100)
|
|
398
|
+
x = center_x + radius * np.cos(theta)
|
|
399
|
+
y = center_y + radius * np.sin(theta)
|
|
400
|
+
ax.plot(x*sim.UL/sim.AU, y*sim.UL/sim.AU, color='black', linewidth=2)
|
|
401
|
+
|
|
402
|
+
ax.set_xlabel(xlabel+' [AU]')
|
|
403
|
+
ax.set_ylabel(ylabel+' [AU]')
|
|
404
|
+
#ax.axis('equal')
|
|
405
|
+
|
|
406
|
+
if interpolate and show_streamlines and stream_obj is not None and vmag is not None:
|
|
407
|
+
# Colorbar for velocity magnitude (streamlines)
|
|
408
|
+
cbar = fig.colorbar(stream_obj.lines, ax=ax, label=r'$|v|$ [km/s]')
|
|
409
|
+
else:
|
|
410
|
+
# Colorbar for main map
|
|
411
|
+
if map_type == 'Densidad':
|
|
412
|
+
cbar_label = r'$\log_{10}(\rho) [g/cm^3]$'
|
|
413
|
+
elif map_type == 'Energia':
|
|
414
|
+
cbar_label = r'$\log_{10}(\mathrm{energy})$'
|
|
415
|
+
else:
|
|
416
|
+
cbar_label = f'{vel_comp} [AU]'
|
|
417
|
+
fig.colorbar(pcm, ax=ax, label=cbar_label)
|
|
418
|
+
|
|
419
|
+
fargopy.Plot.fargopy_mark(ax)
|
|
420
|
+
plt.show()
|
|
421
|
+
|
|
422
|
+
# --- Events ---
|
|
423
|
+
update_button.on_click(plot_density)
|
|
424
|
+
slice_text.on_submit(plot_density)
|
|
425
|
+
show_circle_toggle.observe(plot_density, names='value')
|
|
426
|
+
interp_toggle.observe(plot_density, names='value')
|
|
427
|
+
streamlines_toggle.observe(plot_density, names='value')
|
|
428
|
+
cmap_dropdown.observe(plot_density, names='value')
|
|
429
|
+
map_dropdown.observe(plot_density, names='value')
|
|
430
|
+
vel_dropdown.observe(plot_density, names='value')
|
|
431
|
+
|
|
432
|
+
# --- Display inicial ---
|
|
433
|
+
display(
|
|
434
|
+
time_slider, slice_text, res_slider, interp_toggle, streamlines_toggle,
|
|
435
|
+
density_slider, hill_frac_slider, show_circle_toggle, cmap_dropdown, map_dropdown, vel_dropdown, progress, update_button
|
|
436
|
+
)
|
|
437
|
+
plot_density()
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def mesh(sim, snapshot=0, slice='theta=1.56', planet=0, draw_hill=True, hill_frac=1.0,
|
|
441
|
+
figsize=(8,8), point_size=1, line_alpha=0.5, cmap='viridis', show=True):
|
|
442
|
+
"""
|
|
443
|
+
Plot the simulation mesh in the XY plane and (optionally) the planet Hill circle.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
sim : Simulation
|
|
448
|
+
The simulation object.
|
|
449
|
+
snapshot : int, optional
|
|
450
|
+
Snapshot to plot, by default 0.
|
|
451
|
+
slice : str, optional
|
|
452
|
+
Slice definition, by default 'theta=1.56'.
|
|
453
|
+
planet : int or str, optional
|
|
454
|
+
Planet index or name to focus, by default 0.
|
|
455
|
+
draw_hill : bool, optional
|
|
456
|
+
Whether to draw the Hill sphere, by default True.
|
|
457
|
+
hill_frac : float, optional
|
|
458
|
+
Fraction of Hill radius to draw, by default 1.0.
|
|
459
|
+
figsize : tuple, optional
|
|
460
|
+
Figure size, by default (8,8).
|
|
461
|
+
point_size : int, optional
|
|
462
|
+
Size of mesh points, by default 1.
|
|
463
|
+
line_alpha : float, optional
|
|
464
|
+
Alpha transparency of mesh lines, by default 0.5.
|
|
465
|
+
cmap : str, optional
|
|
466
|
+
Colormap for points, by default 'viridis'.
|
|
467
|
+
show : bool, optional
|
|
468
|
+
Whether to show the plot, by default True.
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
tuple
|
|
473
|
+
(fig, ax, nr_celdas_radial, nr_celdas_azimutal, n_inside)
|
|
474
|
+
Matplotlib figure and axes, max contiguous radial cells, max contiguous azimuthal cells,
|
|
475
|
+
and the number of mesh cells inside the hill_frac * Hill radius.
|
|
476
|
+
|
|
477
|
+
Examples
|
|
478
|
+
--------
|
|
479
|
+
>>> fp.Plot.mesh(sim, snapshot=0)
|
|
480
|
+
"""
|
|
481
|
+
import matplotlib.pyplot as plt
|
|
482
|
+
import matplotlib.patches as patches
|
|
483
|
+
import numpy as np
|
|
484
|
+
|
|
485
|
+
# Load a 2D interpolated field (keeps same interface used elsewhere)
|
|
486
|
+
gasdens = sim.load_field(fields=['gasdens'], snapshot=snapshot, slice=slice, interpolate=True)
|
|
487
|
+
|
|
488
|
+
# Expect interpolator result with var1_mesh / var2_mesh (as used in plot_interactive)
|
|
489
|
+
try:
|
|
490
|
+
X = gasdens.var1_mesh[0]
|
|
491
|
+
Y = gasdens.var2_mesh[0]
|
|
492
|
+
except Exception:
|
|
493
|
+
# Fallback: if a raw Field-like object is returned with mesh names var1_mesh/var2_mesh attributes
|
|
494
|
+
X = getattr(gasdens, 'var1_mesh', None)
|
|
495
|
+
Y = getattr(gasdens, 'var2_mesh', None)
|
|
496
|
+
if X is None or Y is None:
|
|
497
|
+
raise RuntimeError("Could not obtain var1_mesh/var2_mesh from loaded field. Use interpolate=True and a valid slice.")
|
|
498
|
+
|
|
499
|
+
# Prepare figure
|
|
500
|
+
plt.close('all')
|
|
501
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
502
|
+
|
|
503
|
+
# Plot points (convert to AU for axis if simulation units defined)
|
|
504
|
+
scale = getattr(sim, 'UL', 1.0) / getattr(sim, 'AU', 1.0)
|
|
505
|
+
ax.scatter((X * scale).ravel(), (Y * scale).ravel(), s=point_size, c=(X*0+0.5).ravel(),
|
|
506
|
+
cmap=cmap, marker='.', linewidths=0)
|
|
507
|
+
|
|
508
|
+
# If mesh is 2D arrays, draw grid lines
|
|
509
|
+
if X.ndim == 2 and Y.ndim == 2:
|
|
510
|
+
# rows
|
|
511
|
+
for i in range(X.shape[0]):
|
|
512
|
+
ax.plot(X[i, :]*scale, Y[i, :]*scale, color='gray', linewidth=0.5, alpha=line_alpha)
|
|
513
|
+
# columns
|
|
514
|
+
for j in range(X.shape[1]):
|
|
515
|
+
ax.plot(X[:, j]*scale, Y[:, j]*scale, color='gray', linewidth=0.5, alpha=line_alpha)
|
|
516
|
+
|
|
517
|
+
# Planet selection
|
|
518
|
+
planets = sim.load_planets(snapshot=snapshot)
|
|
519
|
+
center_x = center_y = None
|
|
520
|
+
radius = 0.0
|
|
521
|
+
if planets:
|
|
522
|
+
sel = None
|
|
523
|
+
if isinstance(planet, int):
|
|
524
|
+
try:
|
|
525
|
+
sel = planets[planet]
|
|
526
|
+
except Exception:
|
|
527
|
+
sel = planets[0]
|
|
528
|
+
else:
|
|
529
|
+
# name lookup
|
|
530
|
+
for p in planets:
|
|
531
|
+
if getattr(p, 'name', None) == planet:
|
|
532
|
+
sel = p
|
|
533
|
+
break
|
|
534
|
+
if sel is None:
|
|
535
|
+
sel = planets[0]
|
|
536
|
+
# planet object expected to have pos.x / pos.y and hill_radius property
|
|
537
|
+
center_x = sel.pos.x
|
|
538
|
+
center_y = sel.pos.y
|
|
539
|
+
if draw_hill:
|
|
540
|
+
radius = hill_frac * getattr(sel, 'hill_radius', 0.0)
|
|
541
|
+
|
|
542
|
+
# Draw Hill circle if requested and compute counts
|
|
543
|
+
nr_celdas_radial = 0
|
|
544
|
+
nr_celdas_azimutal = 0
|
|
545
|
+
n_inside = 0
|
|
546
|
+
if draw_hill and center_x is not None and center_y is not None and radius > 0:
|
|
547
|
+
circle = patches.Circle((center_x*scale, center_y*scale), radius*scale,
|
|
548
|
+
edgecolor='red', facecolor='lightblue', linestyle='-', linewidth=1.5)
|
|
549
|
+
ax.add_patch(circle)
|
|
550
|
+
|
|
551
|
+
# Count mesh cells (points) inside the requested fraction of Hill radius
|
|
552
|
+
try:
|
|
553
|
+
# X,Y are in simulation length units (same as center_x, center_y)
|
|
554
|
+
mask_inside = ((X - center_x)**2 + (Y - center_y)**2) <= (radius**2)
|
|
555
|
+
n_inside = int(np.count_nonzero(mask_inside))
|
|
556
|
+
|
|
557
|
+
# If mesh is structured 2D array, compute contiguous runs:
|
|
558
|
+
if X.ndim == 2 and Y.ndim == 2:
|
|
559
|
+
# Helper to get max contiguous True length in a 1D boolean array
|
|
560
|
+
def max_contiguous_true(arr1d):
|
|
561
|
+
idx = np.flatnonzero(arr1d)
|
|
562
|
+
if idx.size == 0:
|
|
563
|
+
return 0
|
|
564
|
+
splits = np.split(idx, np.where(np.diff(idx) > 1)[0] + 1)
|
|
565
|
+
lengths = [s.size for s in splits]
|
|
566
|
+
return max(lengths) if lengths else 0
|
|
567
|
+
|
|
568
|
+
# Azimutal: along rows (axis 1) -> for each row find longest contiguous True segment
|
|
569
|
+
max_az = 0
|
|
570
|
+
for i in range(mask_inside.shape[0]):
|
|
571
|
+
l = max_contiguous_true(mask_inside[i, :])
|
|
572
|
+
if l > max_az:
|
|
573
|
+
max_az = l
|
|
574
|
+
nr_celdas_azimutal = max_az
|
|
575
|
+
|
|
576
|
+
# Radial: along cols (axis 0) -> for each col find longest contiguous True segment
|
|
577
|
+
max_rad = 0
|
|
578
|
+
for j in range(mask_inside.shape[1]):
|
|
579
|
+
l = max_contiguous_true(mask_inside[:, j])
|
|
580
|
+
if l > max_rad:
|
|
581
|
+
max_rad = l
|
|
582
|
+
nr_celdas_radial = max_rad
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"Warning computing counts: {e}")
|
|
586
|
+
|
|
587
|
+
fargopy.Plot.fargopy_mark(ax)
|
|
588
|
+
ax.set_aspect('equal')
|
|
589
|
+
ax.set_xlabel('x [AU]')
|
|
590
|
+
ax.set_ylabel('y [AU]')
|
|
591
|
+
|
|
592
|
+
if show:
|
|
593
|
+
plt.show()
|
|
594
|
+
|
|
595
|
+
return fig, ax, nr_celdas_radial, nr_celdas_azimutal, n_inside
|