Source code for radiosim.ppdisks.ppdisks

# Code adapted from Andreas Maisinger (TU Dortmund University)

import numpy as np

try:
    import torch
except ImportError as err:  # pragma: no cover
    raise ModuleNotFoundError(
        "You need torch to use the ppdisks module of radiosim!"
        'Use uv pip install "radiosim[torch]" to install the necessary packages!'
    ) from err

from astropy.convolution import Gaussian2DKernel
from torch.fft import fft2, ifft2
from torchvision.transforms.functional import resize
from tqdm.autonotebook import tqdm

__all__ = ["create_proto", "generate_proto_set"]


[docs] def generate_proto_set( img_size: int, size: int, alpha_range: tuple = (0, 180), ratio_range: tuple = (3, 15), size_ratio_range: tuple = (0.1, 1), device: str = "cpu", seed: int = 1337, verbose: bool = False, ): """ Generates a Set of simulated protoplanetary disk images Parameters ---------- img_size: int The size of the image (height and width) size: int The amount of images to simulate alpha_range: tuple of float, optional The range of values of the inclination of the disk in degrees. Default: (0, 180) ratio_range: tuple of float, optional The range of values of the ratio the minor axis should be of the major axis :math:`a / b`. Default: (3, 15) size_ratio_range: tuple of float, optional The range of values of the ratio of the image size the disk should take up. Default: (0.1, 1) device: str, optional The name of the device to run the simulations on (with torch). Default: 'cpu' seed: int, optional The seed for the random generator. Default: 1337 verbose: bool, optional Whether to show a progress bar or not. Default: False Returns ------- protos: list of torch.tensor List of the simulated images params: dict The used simulation parameters """ np.random.seed(seed) bundles = tqdm(range(size)) if verbose else range(size) outputs = [ create_proto( img_size, alpha=np.random.randint(*alpha_range), ratio=np.random.randint(*ratio_range) / np.random.randint(*ratio_range), size_ratio=np.random.uniform(*size_ratio_range), device=device, ) for i in bundles ] protos = [out.cpu().numpy() for out in outputs] params = dict( seed=seed, N=size, img_size=img_size, alpha_range=alpha_range, ratio_range=ratio_range, size_ratio_range=size_ratio_range, ) return protos, params
[docs] def create_proto( img_size: int, alpha: float, ratio: float, size_ratio: float, device: str = "cpu", seed: int = None, ): """ Generates a simulated protoplanetary disk image Parameters ---------- img_size: int The size of the image (height and width) alpha: float The inclination angle of the disk in degrees ratio: float The ratio the minor axis should be of the major axis a / b size_ratio: float The ratio of the image size the disk should take up device: str, optional The name of the device to run the simulations on (with torch) seed: int, optional The seed for the random generator. If set to ``None``, the seed is ignored. Returns ------- proto: torch.tensor The simulated image of the protoplanetary disk """ if seed is not None: np.random.seed(seed) fix_size = 900 center = (fix_size // 2, fix_size // 2) m, n = _get_m_n() core, d_core = _sim_core(fix_size, alpha, center, ratio, device=device) sc = np.random.randint(5, 10) core = _smooth(sc, sc, core, device=device) * _get_scale(d_core, m, n) num_circ = int(np.random.normal(3, 0.5) // 1) els_full = torch.zeros((900, 900), device=device) ds = [] a_max = d_core for i in range(num_circ): eli, di, maxlength = _sim_ring( fix_size, alpha, center, ratio, m, n, (i + 1) * 30, device=device ) a_max = np.max([maxlength, a_max]) els_full += eli ds.append(di) proto = core + els_full proto = torch.abs(proto) img_size2 = int(fix_size // 2) a_max = np.min([img_size2, a_max]) proto_snip = proto[ int(img_size2 - a_max) : int(img_size2 + a_max), int(img_size2 - a_max) : int(img_size2 + a_max), ] if proto_snip.max() < 1e-10: return create_proto(img_size, alpha, ratio, size_ratio) new_size = int(size_ratio * img_size) new_size = new_size if new_size % 2 == 0 else new_size - 1 proto_snip = proto_snip[None, :, :] proto_snip = resize(proto_snip, (new_size, new_size)) proto_snip = proto_snip[0] proto_snip_size2 = int(proto_snip.shape[0] // 2) img_size2 = int(img_size // 2) proto = torch.ones((img_size, img_size), device=device) * 1e-10 proto[ img_size2 - proto_snip_size2 : img_size2 + proto_snip_size2, img_size2 - proto_snip_size2 : img_size2 + proto_snip_size2, ] += proto_snip del proto_snip return proto.cpu()
def _create_ellipse( img_size: int, a: float, b: float, center: tuple, alpha: float, device: str = "cpu", ): """ Generates an ellipse Parameters ---------- img_size: int The size of the image (height and width) a: float The major axis of the ellipse b: float The minor axis of the ellipse center: tuple of float The center of the ellipse alpha: float The rotation angle of the ellipse device: str, optional The name of the device to run the simulations on (with torch) Returns ------- ellipse: torch.tensor The image of the ellipse average_distance: float The average distance from the center of the ellipse """ x_0 = center[0] y_0 = center[1] xx, yy = torch.from_numpy(np.mgrid[:img_size, :img_size]).to(device) term1 = ( ((xx - x_0) * np.cos(np.deg2rad(alpha))) + ((yy - y_0) * np.sin(np.deg2rad(alpha))) ) ** 2 term2 = ( ((xx - x_0) * np.sin(np.deg2rad(alpha))) - ((yy - y_0) * np.cos(np.deg2rad(alpha))) ) ** 2 ellipse = ((term1 / a**2) + (term2 / b**2)) <= 1 avarage_distance = (a + b) / 2 return ellipse, avarage_distance def _substract( ellipse1: torch.tensor, ellipse2: torch.tensor, distance1: float, distance2: float ): """ Substracts two ellipses from eachother Parameters ---------- ellipse1: torch.tensor The first ellipse ellipse2: torch.tensor The second ellipse distance1: float The average distance from the center of the first ellipse distance1: float The average distance from the center of the second ellipse Returns ------- subtracted: torch.tensor The result of the subtraction average_distance: float The average distance from the center of the ellipse resulting from the subtraction """ subtracted = ellipse1 ^ ellipse2 average_distance = (distance1 + distance2) / 2 return subtracted, average_distance def _sim_core( img_size: int, alpha: float, center: tuple, ratio: float, device: str = "cpu" ): """ Simulates the core of the protoplanetary disk img_size: int The size of the image (height and width) alpha: float The rotation angle of the core center: tuple of float The center of the core ratio: float The ratio the minor axis should be of the major axis a / b device: str, optional The name of the device to run the simulations on (with torch) Returns ------- core: torch.tensor The image of the core d_core: float The average distance from the center of the core """ a = np.float64(np.random.randint(3, 15)) core, d_core = _create_ellipse(img_size, a, a / ratio, center, alpha, device=device) return core, d_core def _sim_ring( img_size: int, alpha: float, center: tuple, ratio: float, m: float, n: float, initial_a: float = 0, device: str = "cpu", ): """ Simulates a ring of the protoplanetary disk img_size: int The size of the image (height and width) alpha: float The rotation angle of the core center: tuple of float The center of the core ratio: float The ratio the minor axis should be of the major axis a / b m: float Empirical value determining the scale of the ring n: float Empirical value determining the scale of the ring initial_a: float, optional The initial size of the major axis device: str, optional The name of the device to run the simulations on (with torch) Returns ------- ring: torch.tensor The image of the ring d_ring: float The average distance from the center of the ring max_value: float Largest extent of the ring """ a = np.random.randint(10, 35) + initial_a exp = np.random.randint(5, 10) ring_a, d_ring_a = _create_ellipse( img_size, a, a / ratio, center, alpha, device=device ) ring_b, d_ring_b = _create_ellipse( img_size, a + exp, (a + exp) / ratio, center, alpha, device=device ) ring, d_ring = _substract(ring_b, ring_a, d_ring_b, d_ring_a) s = _get_smooth_factor(a) ring = _smooth(s, s, ring, device=device) * _get_scale(d_ring, m, n) max_value = np.max([a, a + exp, a / ratio, (a + exp) / ratio]) return ring, d_ring, max_value + 6 * s def _get_smooth_factor(a: float): """ Empirically determines the standard deviation for the gaussian smoothing. """ if a < 15: return 2 if a < 20: return 4 if a < 30: return 6 if a < 40: return 9 if a >= 40: return 12 # from https://stackoverflow.com/a/47979802 def _convolve2d(x: torch.tensor, y: torch.tensor): """ Convolves two 2-dimensional images with eachother Parameters ---------- x: torch.tensor The first image y: torch.tensor The second image Returns ------- cc: torch.tensor The convolved image """ fr = fft2(x) fr2 = fft2(torch.flipud(torch.fliplr(y))) m, n = fr.shape cc = torch.real(ifft2(fr * fr2)) cc = torch.roll(cc, -int(m / 2) + 1, dims=0) cc = torch.roll(cc, -int(n / 2) + 1, dims=1) return cc def _smooth(x: float, y: float, c: torch.tensor, device: str = "cpu"): """ Smooths the input image by convolving it with a gaussian 2d-kernel Parameters ---------- x: float The standard deviation in x-direction y: float The standard deviation in y-direction c: torch.tensor The image to smooth device: str, optional The name of the device to run the simulations on (with torch) """ gauss_kernel = torch.from_numpy( Gaussian2DKernel( x_stddev=x, y_stddev=y, x_size=c.shape[0], y_size=c.shape[1] ).array ).to(device) return _convolve2d(c, gauss_kernel) def _e(x: float, m: float, n: float): """ Computes the value of an exponential function exp(m * x + n) """ return np.exp(m * x + n) def _get_m_n(): """ Get the empirical values for m and n from JSON files """ results = { "average_1": {"m": -0.08908080536281453, "n": -4.808388735775005}, "average_2": {"m": -0.04053517757866287, "n": -5.718461569072661}, "average_3": {"m": -0.023989028595164514, "n": -6.22105762345096}, "average_4": {"m": -0.011198837787046627, "n": -6.701733964123176}, } id = np.random.choice(list(results.keys())) return results[id]["m"], results[id]["n"] def _get_scale(distance: float, m: float, n: float, noise_scale: float = 0.05): """ Get the empirical standard deviation for the smoothing """ return _e(distance, m, n) + np.random.normal(0, _e(distance, m, n) * noise_scale)