rustmatrix 2.1.1

Rust-backed T-matrix scattering for nonspherical particles (port of pytmatrix)
Documentation
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tutorial 10 \u2014 Supercooled liquid water vs. snow at cloud-radar frequencies\n",
    "\n",
    "Reference: Billault-Roux, A.-C., Georgakaki, P., Grazioli, J.,\n",
    "Romanens, G., Sotiropoulou, G., Phillips, V., Nenes, A., and Berne,\n",
    "A., 2023. *Distinct secondary ice production processes observed in\n",
    "radar Doppler spectra: insights from a case study*, Atmos. Chem.\n",
    "Phys., 23, 10207\u201310234 (doi:10.5194/acp-23-10207-2023).\n",
    "\n",
    "Mixed-phase cloud volumes contain supercooled liquid droplets (SLW)\n",
    "and ice particles side by side. The two populations have completely\n",
    "different scattering and fall-speed signatures:\n",
    "\n",
    "* **SLW**: small (\u2272200 \u00b5m), spherical, cold refractive index of\n",
    "  water, falls at a few cm/s. Rayleigh at both X- and W-band \u21d2 very\n",
    "  low Z_h, Z_dr \u2248 0 dB, no polarimetric signature.\n",
    "* **Snow**: millimetre-scale oblate low-density aggregates, falls at\n",
    "  ~0.5\u20131 m/s. Moderately non-Rayleigh at W-band \u21d2 higher Z_h, small\n",
    "  positive Z_dr from the habit anisotropy.\n",
    "\n",
    "This notebook builds dedicated scatterers for each population, shows\n",
    "their \u03c3_b(D) and bulk radar observables at X and W band, and then\n",
    "combines them in a `HydroMix` to produce the bimodal Doppler spectrum\n",
    "that motivates the Billault-Roux et al. case-study analysis.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from rustmatrix import (HydroMix, MixtureComponent, Scatterer,\n",
    "                         SpectralIntegrator, radar, spectra)\n",
    "from rustmatrix.psd import ExponentialPSD, GammaPSD, PSDIntegrator\n",
    "from rustmatrix.refractive import m_w_0C, mi\n",
    "from rustmatrix.tmatrix_aux import (K_w_sqr, geom_vert_back,\n",
    "                                      wl_X, wl_W)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build a supercooled-liquid-water scatterer\n",
    "\n",
    "Small spherical drops (D in 10\u2013200 \u00b5m), refractive index at 0\u00b0C,\n",
    "log-normal-ish gamma PSD centred around 30 \u00b5m. We use mm units\n",
    "throughout to match `rustmatrix` conventions.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "SLW_DMAX = 0.2  # mm\n",
    "\n",
    "def build_slw(wl):\n",
    "    s = Scatterer(wavelength=wl, m=m_w_0C[wl],\n",
    "                  Kw_sqr=K_w_sqr[wl], axis_ratio=1.0,\n",
    "                  ddelt=1e-4, ndgs=2)\n",
    "    integ = PSDIntegrator()\n",
    "    integ.D_max = SLW_DMAX\n",
    "    integ.num_points = 128\n",
    "    integ.geometries = (geom_vert_back,)\n",
    "    s.psd_integrator = integ\n",
    "    s.psd_integrator.init_scatter_table(s)\n",
    "    # Gamma PSD: D0 = 0.03 mm (30 \u00b5m), Nw = 1e11 m\u207b\u00b3 mm\u207b\u00b9, \u03bc = 4\n",
    "    s.psd = GammaPSD(D0=0.03, Nw=1e11, mu=4, D_max=SLW_DMAX)\n",
    "    return s\n",
    "\n",
    "slw_X = build_slw(wl_X)\n",
    "slw_W = build_slw(wl_W)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build a snow scatterer (low-density oblate aggregates)\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "SNOW_DMAX = 8.0\n",
    "RHO_SNOW = 0.2\n",
    "AXIS_SNOW = 0.6\n",
    "\n",
    "def build_snow(wl):\n",
    "    s = Scatterer(wavelength=wl, m=mi(wl, RHO_SNOW),\n",
    "                  Kw_sqr=K_w_sqr[wl], axis_ratio=AXIS_SNOW,\n",
    "                  ddelt=1e-4, ndgs=2)\n",
    "    integ = PSDIntegrator()\n",
    "    integ.D_max = SNOW_DMAX\n",
    "    integ.num_points = 256\n",
    "    integ.geometries = (geom_vert_back,)\n",
    "    s.psd_integrator = integ\n",
    "    s.psd_integrator.init_scatter_table(s)\n",
    "    s.psd = ExponentialPSD(N0=5e3, Lambda=1.0, D_max=SNOW_DMAX)\n",
    "    return s\n",
    "\n",
    "snow_X = build_snow(wl_X)\n",
    "snow_W = build_snow(wl_W)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \u03c3_b(D) \u2014 Rayleigh vs. Mie across the two populations\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def sigma_b(wl, D_mm, m, axis_ratio):\n",
    "    s = Scatterer(radius=D_mm/2, wavelength=wl, m=m,\n",
    "                  Kw_sqr=K_w_sqr[wl], axis_ratio=axis_ratio,\n",
    "                  ddelt=1e-4, ndgs=2)\n",
    "    s.set_geometry(geom_vert_back)\n",
    "    return radar.radar_xsect(s)\n",
    "\n",
    "D_slw = np.linspace(0.005, SLW_DMAX, 60)\n",
    "D_snow = np.linspace(0.1, SNOW_DMAX, 200)\n",
    "\n",
    "sb_slw_X = np.array([sigma_b(wl_X, d, m_w_0C[wl_X], 1.0) for d in D_slw])\n",
    "sb_slw_W = np.array([sigma_b(wl_W, d, m_w_0C[wl_W], 1.0) for d in D_slw])\n",
    "sb_snow_X = np.array([sigma_b(wl_X, d, mi(wl_X, RHO_SNOW), AXIS_SNOW)\n",
    "                       for d in D_snow])\n",
    "sb_snow_W = np.array([sigma_b(wl_W, d, mi(wl_W, RHO_SNOW), AXIS_SNOW)\n",
    "                       for d in D_snow])\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7.5, 4.5))\n",
    "ax.loglog(D_slw, sb_slw_X, 'C0-',  label='SLW, X-band')\n",
    "ax.loglog(D_slw, sb_slw_W, 'C0--', label='SLW, W-band')\n",
    "ax.loglog(D_snow, sb_snow_X, 'C3-',  label='snow, X-band')\n",
    "ax.loglog(D_snow, sb_snow_W, 'C3--', label='snow, W-band')\n",
    "ax.set_xlabel('diameter D [mm]')\n",
    "ax.set_ylabel(r'$\\sigma_b$ [mm\u00b2]')\n",
    "ax.set_title('Single-particle backscatter: SLW droplets vs. snow aggregates')\n",
    "ax.legend(fontsize=9); ax.grid(True, which='both', alpha=0.3)\n",
    "fig.tight_layout();\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bulk radar observables at X and W band\n",
    "\n",
    "Z_h(SLW) is ~40+ dB below Z_h(snow) even though the droplet\n",
    "concentration is high \u2014 the D\u2076 Rayleigh penalty at 30-\u00b5m scale kills\n",
    "the signal. Z_dr is zero for SLW (spheres) but positive for snow\n",
    "(oblate). Dual-wavelength ratio DWR = Z_X \u2212 Z_W is near zero for SLW\n",
    "and positive for snow (non-Rayleigh at W-band).\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def bulk(s):\n",
    "    s.set_geometry(geom_vert_back)\n",
    "    return 10 * np.log10(radar.refl(s))\n",
    "\n",
    "print(f'{\"\":<10} {\"Z_h X [dBZ]\":>12} {\"Z_h W [dBZ]\":>12} '\n",
    "      f'{\"DWR [dB]\":>10}')\n",
    "for name, sX, sW in [('SLW',  slw_X,  slw_W),\n",
    "                      ('snow', snow_X, snow_W)]:\n",
    "    zX = bulk(sX); zW = bulk(sW)\n",
    "    print(f'{name:<10} {zX:>12.2f} {zW:>12.2f} {zX - zW:>10.2f}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bimodal Doppler spectrum of the SLW + snow mixture\n",
    "\n",
    "A `HydroMix` with per-component fall-speed and turbulence captures\n",
    "the two populations cleanly: SLW pins to v \u2248 0 with a narrow spread,\n",
    "snow sits near 0.5\u20131.0 m/s with a broader tail. Plotted in dBZ space\n",
    "the two modes are clearly separable even with realistic noise.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Use W-band for the spectrum \u2014 best velocity-wise resolution of the\n",
    "# SLW mode, and non-Rayleigh snow signature is most evident here.\n",
    "mix = HydroMix([\n",
    "    MixtureComponent(slw_W,  slw_W.psd,  'slw'),\n",
    "    MixtureComponent(snow_W, snow_W.psd, 'snow'),\n",
    "])\n",
    "\n",
    "integ = SpectralIntegrator(\n",
    "    mix,\n",
    "    component_kinematics={\n",
    "        'slw':  (lambda D: 0.003 * (D / 0.01) ** 2,\n",
    "                 spectra.GaussianTurbulence(0.1)),\n",
    "        'snow': (spectra.fall_speed.locatelli_hobbs_1974_aggregates,\n",
    "                 spectra.GaussianTurbulence(0.2)),\n",
    "    },\n",
    "    v_min=-1.0, v_max=3.0, n_bins=1024,\n",
    "    geometry_backscatter=geom_vert_back,\n",
    "    noise='realistic',\n",
    ").run()\n",
    "\n",
    "def dBZ(x):\n",
    "    return 10 * np.log10(np.maximum(x, 1e-12))\n",
    "\n",
    "# Run each population on its own (no noise) so each mode's true peak\n",
    "# is unambiguous. The HydroMix spectrum is dominated by snow (~50 dB\n",
    "# louder than SLW), so looking for the SLW peak on the combined,\n",
    "# noise-added spectrum would just pick up the snow tail crossing\n",
    "# through the near-zero velocity range.\n",
    "slw_only = SpectralIntegrator(\n",
    "    slw_W,\n",
    "    fall_speed=lambda D: 0.003 * (D / 0.01) ** 2,\n",
    "    turbulence=spectra.GaussianTurbulence(0.1),\n",
    "    v_min=-1.0, v_max=3.0, n_bins=1024,\n",
    "    geometry_backscatter=geom_vert_back,\n",
    ").run()\n",
    "snow_only = SpectralIntegrator(\n",
    "    snow_W,\n",
    "    fall_speed=spectra.fall_speed.locatelli_hobbs_1974_aggregates,\n",
    "    turbulence=spectra.GaussianTurbulence(0.2),\n",
    "    v_min=-1.0, v_max=3.0, n_bins=1024,\n",
    "    geometry_backscatter=geom_vert_back,\n",
    ").run()\n",
    "\n",
    "v_slw  = slw_only.v[int(np.argmax(slw_only.sZ_h))]\n",
    "v_snow = snow_only.v[int(np.argmax(snow_only.sZ_h))]\n",
    "print(f'SLW mode peak  v = {v_slw:+.3f} m/s')\n",
    "print(f'snow mode peak v = {v_snow:+.3f} m/s')\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 4))\n",
    "ax.plot(slw_only.v, dBZ(slw_only.sZ_h), 'C2-', lw=1.3, label='SLW only')\n",
    "ax.plot(snow_only.v, dBZ(snow_only.sZ_h), 'C3-', lw=1.3, label='snow only')\n",
    "ax.plot(integ.v, dBZ(integ.sZ_h), 'C0-', lw=2.0,\n",
    "        label='SLW + snow (HydroMix, with noise)')\n",
    "ax.axvline(v_slw,  color='C2', alpha=0.4, ls=':',\n",
    "           label=f'SLW peak ({v_slw:+.2f})')\n",
    "ax.axvline(v_snow, color='C3', alpha=0.4, ls=':',\n",
    "           label=f'snow peak ({v_snow:+.2f})')\n",
    "ax.axhline(dBZ(integ.noise_h / (integ.v[-1] - integ.v[0])),\n",
    "           color='k', ls='--', alpha=0.5, label='noise floor')\n",
    "ax.set_xlabel('Doppler velocity v [m/s]')\n",
    "ax.set_ylabel('sZ_h [dBZ / (m/s)]')\n",
    "ax.set_title('W-band SLW vs. snow vs. combined Doppler spectrum')\n",
    "ax.legend(fontsize=9, loc='upper right'); ax.grid(True, alpha=0.3)\n",
    "fig.tight_layout();\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Takeaway\n",
    "\n",
    "The Billault-Roux et al. 2023 case study relies on the *spectral*\n",
    "separability demonstrated here: a narrow near-zero Doppler peak is\n",
    "SLW, a broader fall-velocity peak is ice; the polarimetric spectral\n",
    "variables further distinguish columnar (high SLDR) from planar and\n",
    "aggregated ice. With the two populations built as independent\n",
    "rustmatrix scatterers you can plug them into `HydroMix` and recover\n",
    "bulk radar observables that match what a vertically-pointing radar\n",
    "would measure, or analyse the spectral signatures by looking at the\n",
    "individual modes.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}