33from os import PathLike
44from pathlib import Path
55
6+ import matplotlib
7+ import matplotlib .animation as animation
8+ import matplotlib .pyplot as plt
69import numpy as np
710from astropy import constants as const
811from astropy import units as un
12+ from tqdm .auto import tqdm
913
1014from radiosim .ppdisks .config import TOMLConfiguration
15+ from radiosim .ppdisks .plotting .plotting import plot_polar_image
1116
1217from .config import Variables
1318from .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