Ram Pressure Shadowing ``shadows`` ================================== When a wind slams into a galaxy disk and strips gaseous material, it doesn't uniformly pass through the disk. Therefore, material along the wind direction *beyond the disk* will be "shielded" to some degree from the wind. This shiedling, which is modelled in ``GalaRP`` with the ``ShadowBase`` class and associated subclasses, tries to capture this by limiting the ram pressure strength evaluated at various physical positions. ``GalaRP`` has a large suite of built-in shadow classes for the user to try, which range from shadow strengths that are uniform across the shadowed region, to exponential drop-offs, to a shadow that loses strength the higher a particle goes above the disk. It is also easy for the user to define their own subclasses using the ``ShadowBase`` class. Dynamic Shadow -------------- The main shadow the user should apply is the ``DynamicShadow``, which automatically determines the shadow based on the particle distributions and the wind. It does this by rotating the particles into the wind's frame of reference, determines sightlines along the wind direction by binning the particles, and then applies a damping for the given particles based on the cumulative distribution along that line of sight. .. code-block:: python shadow = grp.DynamicShadow(wind, depth=0.02, n_bins=20) There are "static shadows" which the remainder of this page covers. These are kept in as simple shadowing prescrptions, but many important aspects of a ram pressure event are not replicated using these models. However, they do speed up individual simulations, so toy models can use them for simple exercises. Basic Example ------------- The easiest way to create a shadow is to initialize it using a wind. For example, we will define a simple uniform shadow that is then set up to coincide with a 45 degree Lorentzian wind. .. code-block:: python import galarp as grp wind = grp.LorentzianWind(t0=500 * u.Myr, width=400 * u.Myr, units=galactic) wind.init_from_inc(inclination=np.deg2rad(40), strength = 500 * u.km / u.s) shadow = grp.UniformShadow() shadow.init_from_wind(wind) You pass this into a ``GalaRP`` sim as follows: .. code-block:: python sim = grp.RPSim(shadow=shadow) Dynamic Shadow Based on Stripping Radius ---------------------------------------- The shadow's size should realistically change based on the size of the active disk. For example, a gas disk that started with a 10kpc radius shouldn't still have a 10kpc shadow when the remaining gas disk after stripping is only 2kpc in radius. This is handled in ``GalaRP`` using the ``dynamic`` keyword in the ``ShadowBase`` class. Setting this keyword to True will have the shadow, at each evaluation, determine the stripping radius of the disk on the fly using ``grp.calculate_rstrip(...)`` and will set the disk radius of the shadow accordingly. .. code-block:: python shadow = grp.UniformShadow(dynamic=True) shadow.init_from_wind(wind) You can see this in action in the following plot, where there stripping radius is shown as a black circle that evolves to shrink as the disk is stripped. .. raw:: html and the evolution of the stripping radius can be shown for each Runge-Kutta iteration (which are taken directly from a ``GalaRP`` run by appending ``self.Rdisk`` at each call of ``shadow.evaluate()``. This can be done by setting ``debug=True`` in the shadow class. .. raw:: html User-Defined shadows -------------------- To define a shadow, simply call the ``ShadowBase`` class as the parent class. The user will need to define the ``evaluate`` method, which determines the shadow strength at a time **t** for a set of positions **q**. It should be noted that the shadow's evaluate function should return values between 0 and 1, where 0 represents *complete shadowing* (i.e. the ram pressure acceleration is reduced to 0) and 1 represents no shadowing. In the ``RPSim`` class, it is literally a multiple applied to each particle's calculated ram pressure acceleration. For an example, this is a an example class for a uniform wind that also has a dropoff based on height above the disk. .. code-block:: python class UniformExponentialZVariableShadow(grp.ShadowBase): def __init__(self, damping=0.5, R_disk=10, zmin=0.5, phi=np.deg2rad(20), z_dropoff=10, **kwargs ): super().__init__(damping=damping, R_disk=R_disk, shadow_name="Uniform", **kwargs) if isinstance(zmin, u.Quantity): zmin = zmin.to(u.kpc).value self.zmin = zmin self.phi = phi self.z_dropoff = z_dropoff self.frac = kwargs.get("frac", 0.9) self.Rmax = kwargs.get("Rmax", 20) self.zmax = kwargs.get("zmax", 2) self.debug = kwargs.get("debug", False) self.Rdisks = [] def evaluate(self, q, t): x, y, z = q.T if self.dynamic_shadow: self.R_disk = grp.calculate_rstrip(q.T, frac=self.frac, rmax=self.Rmax, zmax=self.zmax) if self.debug: self.Rdisks.append(self.R_disk) cent = _shadow_tangent(z, self.phi) dist = np.sqrt((x - cent) ** 2 + y**2) out = np.ones(dist.shape) in_disk = np.logical_and((z > self.zmin), (dist < self.R_disk)) out[in_disk] = self.damping + (1 - np.exp(-z[in_disk] / self.z_dropoff)) return out