Skip to content

Commit ec32cdd

Browse files
committed
feat: Add animations for disk models
1 parent 5ab8616 commit ec32cdd

File tree

3 files changed

+270
-17
lines changed

3 files changed

+270
-17
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import astropy.units as un
2+
import matplotlib
3+
import numpy as np
4+
from numpy.typing import ArrayLike
5+
6+
from .utils import (
7+
configure_axes,
8+
configure_colorbar,
9+
ellipse_img2cartesian_img,
10+
get_norm,
11+
)
12+
13+
14+
def plot_polar_image(
15+
polar_intensities: np.ndarray,
16+
grid_shape: tuple,
17+
r_lims: ArrayLike,
18+
phi_lims: ArrayLike | None = None,
19+
intensity_unit: str | None = None,
20+
a_maj: float = 1.0,
21+
b_min: float = 1.0,
22+
rot_angle: float = 0.0,
23+
xy_lims: ArrayLike | None = None,
24+
xy_unit: un.Unit = un.AU,
25+
dtype: type = np.float64,
26+
cmap: str | matplotlib.colors.Colormap = "inferno",
27+
norm: str | matplotlib.colors.Normalize | None = None,
28+
intensity_limits: tuple | None = None,
29+
fig_args: dict | None = None,
30+
plot_args: dict | None = None,
31+
save_to: str | None = None,
32+
save_args: dict = None,
33+
fig: matplotlib.figure.Figure | None = None,
34+
ax: matplotlib.axes.Axes | None = None,
35+
) -> tuple[matplotlib.image.AxesImage, matplotlib.figure.Figure, matplotlib.axes.Axes]:
36+
if phi_lims is None:
37+
phi_lims = [-np.pi, np.pi]
38+
39+
if xy_lims is None:
40+
xy_lims = ([-r_lims[1], r_lims[1]], [-r_lims[1], r_lims[1]])
41+
42+
norm = get_norm(norm) if isinstance(norm, str) else norm
43+
44+
intensity_unit = "Intensity / a.u." if intensity_unit is None else intensity_unit
45+
46+
save_args = {} if save_args is None else save_args
47+
48+
plot_args = {} if plot_args is None else plot_args
49+
fig_args = {} if fig_args is None else fig_args
50+
51+
r = np.linspace(
52+
r_lims[0],
53+
r_lims[1],
54+
polar_intensities.shape[0],
55+
)
56+
phi = np.linspace(phi_lims[0], phi_lims[1], polar_intensities.shape[1])
57+
58+
rs, phis = np.meshgrid(r, phi)
59+
rs = rs.T
60+
phis = phis.T
61+
62+
data_trafo = ellipse_img2cartesian_img(
63+
r=rs,
64+
phi=phis,
65+
intensities=polar_intensities,
66+
grid_shape=grid_shape,
67+
a=a_maj,
68+
b=b_min,
69+
alpha=rot_angle,
70+
xy_lims=xy_lims,
71+
)
72+
73+
fig, ax = configure_axes(fig=fig, ax=ax, fig_args=fig_args)
74+
75+
im = ax.imshow(
76+
data_trafo,
77+
origin="lower",
78+
cmap=cmap,
79+
interpolation="none",
80+
norm=get_norm(norm=norm)
81+
if intensity_limits is None
82+
else get_norm(norm=norm, vmin=intensity_limits[0], vmax=intensity_limits[1]),
83+
extent=np.ravel(xy_lims),
84+
**plot_args,
85+
)
86+
87+
configure_colorbar(mappable=im, ax=ax, fig=fig, label=intensity_unit)
88+
89+
ax.set_xlabel(f"$x$ / {xy_unit}")
90+
ax.set_ylabel(f"$y$ / {xy_unit}")
91+
92+
if save_to is not None:
93+
fig.savefig(save_to, **save_args)
94+
95+
return im, fig, ax

src/radiosim/ppdisks/plotting/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def configure_axes(
4747
return fig, ax
4848

4949

50-
def _get_norm(
50+
def get_norm(
5151
norm: str,
5252
vmax: float | None = None,
5353
vmin: float | None = None,
@@ -194,13 +194,13 @@ def xy2pix(
194194
shape: tuple[int],
195195
xy_lims: tuple[list[float]] = ([-1, 1], [-1, 1]),
196196
):
197-
xy_lims = np.ndarray(xy_lims)
197+
xy_lims = np.array(xy_lims)
198198

199199
delta_x = (np.abs(np.diff(xy_lims[0])) / shape[1])[0]
200200
delta_y = (np.abs(np.diff(xy_lims[1])) / shape[0])[0]
201201

202-
col_idx = np.floor((x - xy_lims[0, 0]) // delta_x).int()
203-
row_idx = np.floor((y - xy_lims[1, 0]) // delta_y).int()
202+
col_idx = np.int64(np.floor((x - xy_lims[0, 0]) // delta_x))
203+
row_idx = np.int64(np.floor((y - xy_lims[1, 0]) // delta_y))
204204

205205
return row_idx, col_idx
206206

@@ -213,7 +213,7 @@ def ellipse_img2cartesian_img(
213213
a: float,
214214
b: float,
215215
alpha: float,
216-
dtype: type,
216+
dtype: type = np.float64,
217217
xy_lims: tuple[list[float]] = ([-1, 1], [-1, 1]),
218218
):
219219
image = np.zeros(grid_shape, dtype=dtype)

src/radiosim/ppdisks/simulation.py

Lines changed: 170 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
from os import PathLike
44
from pathlib import Path
55

6+
import matplotlib
7+
import matplotlib.animation as animation
8+
import matplotlib.pyplot as plt
69
import numpy as np
710
from astropy import constants as const
811
from astropy import units as un
12+
from tqdm.auto import tqdm
913

1014
from radiosim.ppdisks.config import TOMLConfiguration
15+
from radiosim.ppdisks.plotting.plotting import plot_polar_image
1116

1217
from .config import Variables
1318
from .config.fargo import Constants, Planet, PlanetConfig, UnitSystem
@@ -27,19 +32,21 @@ def get_default_sampling_config():
2732
},
2833
"dust_parameters": {
2934
"invstokes": {
30-
"1": [8.0, 20.0],
35+
"1": [10.0, 20.0],
3136
},
32-
"epsilon": [0.05, 0.2],
37+
"epsilon": [0.05, 0.2], # dust-to-gas ratio,
3338
},
3439
"planet_parameters": {
35-
"binary_ratio": 0.334, # Ratio of binary systems to single systems
36-
"binary_period": [6.04800e5, 3e9], # Seconds (logarithmic sampling)
37-
"binary_eccentricity": [0.0, 0.4], # 0 = Circle, 0 < e < 1 = Ellipse
38-
"stellar_mass": [0.5, 5], # Solar Masses
40+
"binary_ratio": 0.0, # Ratio of binary systems to single systems
41+
"binary_period": [6.04800e5, 3e7], # Seconds (logarithmic sampling)
42+
"binary_eccentricity": [0.0, 0.2], # 0 = Circle, 0 < e < 1 = Ellipse
43+
"stellar_mass": [0.5, 2], # Solar Masses
3944
"stellar_temperature": [2000.0, 3000.0], # Kelvin
40-
"num_planets": [1, 5],
45+
"num_planets": [1, 4],
4146
"planet_mass": [1.0e-6, 5.0e-3], # Solar Masses
4247
"planet_orbit_radius": [6.0, 15.0], # Astronomical Units
48+
# short: PEF -> no other planets allowed closer than R_orbit * PEF
49+
"planet_exclusion_factor": 0.2,
4350
"eccentricity": [0.0, 0.9], # 0 = Circle, 0 < e < 1 = Ellipse
4451
},
4552
"mesh_parameters": {
@@ -643,11 +650,32 @@ def sample_dict(read_dict):
643650
high=planet_sampling["planet_mass"][1],
644651
size=num_planets,
645652
)
646-
planet_parameters["planet_orbit_radius"] = rng.uniform(
647-
low=planet_sampling["planet_orbit_radius"][0],
648-
high=planet_sampling["planet_orbit_radius"][1],
649-
size=num_planets,
650-
)
653+
654+
planet_orbits = np.zeros(num_planets)
655+
656+
# This makes sure that planets are not too close to each other
657+
for i_planet in range(num_planets):
658+
valid = False
659+
660+
while not valid:
661+
orbit = rng.uniform(
662+
low=planet_sampling["planet_orbit_radius"][0],
663+
high=planet_sampling["planet_orbit_radius"][1],
664+
)
665+
666+
# if new orbit violates planet exclusion zones around other planets
667+
if np.any(
668+
np.abs(planet_orbits[:i_planet] - orbit) / planet_orbits[:i_planet]
669+
> planet_sampling["planet_exclusion_factor"]
670+
):
671+
continue
672+
else:
673+
valid = True
674+
675+
planet_orbits[i_planet] = orbit
676+
677+
planet_parameters["planet_orbit_radius"] = planet_orbits
678+
651679
planet_parameters["eccentricity"] = rng.uniform(
652680
low=planet_sampling["eccentricity"][0],
653681
high=planet_sampling["eccentricity"][1],
@@ -701,6 +729,136 @@ def __init__(self, id: int, run: SimulationRun):
701729
self._directory: Path = run._directory / f"model_{id}"
702730
self._run: SimulationRun = run
703731

732+
def get_num_outputs(self) -> int:
733+
files = {
734+
f
735+
for f in Path(self.get_data_directory()).glob("gasdens*.dat")
736+
if f.is_file() and "2d" not in f.name
737+
}
738+
return len(files)
739+
740+
def get_dust_density(self, output_idx: int = -1, dust_idx: int = 1) -> np.ndarray:
741+
if output_idx < 0:
742+
output_idx = np.arange(0, self.get_num_outputs())[output_idx]
743+
744+
return np.fromfile(
745+
self.get_data_directory() / f"dust{dust_idx}dens{output_idx}.dat",
746+
dtype=self._run.get_float_type(),
747+
).reshape(self._run.get_polar_img_size())
748+
749+
def plot_dust_density(
750+
self,
751+
grid_shape: tuple,
752+
output_idx: int = -1,
753+
dust_idx: int = 1,
754+
xy_unit: un.Unit = un.AU,
755+
save_to: str | None = None,
756+
save_args: dict = None,
757+
**kwargs,
758+
) -> tuple[
759+
matplotlib.image.AxesImage, matplotlib.figure.Figure, matplotlib.axes.Axes
760+
]:
761+
r_min, r_max = self.get_radius_lims()
762+
r_min = (r_min * un.AU).to(xy_unit).value
763+
r_max = (r_max * un.AU).to(xy_unit).value
764+
765+
unit_system = self._run._sim._unit_system
766+
767+
return plot_polar_image(
768+
polar_intensities=self.get_dust_density(
769+
output_idx=output_idx, dust_idx=dust_idx
770+
),
771+
grid_shape=grid_shape,
772+
r_lims=[r_min, r_max],
773+
intensity_unit=(
774+
"Dust density / "
775+
f"{
776+
(unit_system.mass / unit_system.length**2).to_string(format='latex')
777+
}"
778+
),
779+
dtype=self._run.get_float_type(),
780+
save_to=save_to,
781+
save_args=save_args,
782+
**kwargs,
783+
)
784+
785+
def animate_dust_density(
786+
self,
787+
grid_shape: tuple,
788+
save_to: str | PathLike,
789+
step_size: int,
790+
dust_idx: int = 1,
791+
xy_unit: un.Unit = un.AU,
792+
save_args: dict = None,
793+
fps: int = 30,
794+
dpi: int | str = "figure",
795+
blit: bool = True,
796+
show_progress: bool = True,
797+
**kwargs,
798+
) -> None:
799+
save_to = Path(save_to)
800+
801+
ims = []
802+
803+
fig, ax = plt.subplots()
804+
805+
print(
806+
"Animation length will be: "
807+
f"{np.round(self.get_num_outputs() // step_size / fps, 2)} seconds"
808+
)
809+
810+
for i in tqdm(
811+
np.arange(start=0, stop=self.get_num_outputs(), step=step_size),
812+
desc="Plotting densities",
813+
disable=not show_progress,
814+
):
815+
im, _, _ = self.plot_dust_density(
816+
grid_shape=grid_shape,
817+
output_idx=i,
818+
dust_idx=dust_idx,
819+
xy_unit=xy_unit,
820+
fig=fig,
821+
ax=ax,
822+
**kwargs,
823+
)
824+
ims.append([im])
825+
if hasattr(im, "colorbar") and im.colorbar is not None:
826+
im.colorbar.ax.remove()
827+
828+
anim = animation.ArtistAnimation(fig, ims, blit=blit, interval=1e3 / fps)
829+
830+
writer = None
831+
if save_to.suffix.lower() == ".gif":
832+
writer = animation.PillowWriter(
833+
fps=fps,
834+
bitrate=-1,
835+
)
836+
writer.setup(fig=fig, outfile=save_to, dpi=dpi)
837+
838+
def _progress_func(_i, _n):
839+
progress_bar.update(1)
840+
841+
with tqdm(
842+
total=len(ims), desc="Saving animation", disable=not show_progress
843+
) as progress_bar:
844+
if writer is None:
845+
anim.save(save_to, progress_callback=_progress_func, dpi=dpi)
846+
else:
847+
anim.save(
848+
save_to, progress_callback=_progress_func, writer=writer, dpi=dpi
849+
)
850+
851+
def get_radius_lims(self) -> tuple[float]:
852+
sample_config = self.get_sample_config()
853+
854+
r_min = sample_config["mesh_parameters.y_min"]
855+
r_max = (
856+
np.max(sample_config["planet_parameters.planet_orbit_radius"])
857+
* sample_config["mesh_parameters.y_max_ratio"]
858+
)
859+
860+
return r_min, r_max
861+
704862
def get_sample_config(self) -> TOMLConfiguration:
705863
return TOMLConfiguration(self._directory / "samples.toml")
706864

0 commit comments

Comments
 (0)