diff --git a/examples/additive_coag_comparison.ipynb b/examples/additive_coag_comparison.ipynb index f17d161a..999bfcf5 100644 --- a/examples/additive_coag_comparison.ipynb +++ b/examples/additive_coag_comparison.ipynb @@ -1,790 +1,1269 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "1a90349d", - "metadata": {}, - "source": [ - "[![nbviewer](https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg)](https://nbviewer.jupyter.org/github/open-atmos/PyPartMC/blob/main/examples/additive_coag_comparison.ipynb) \n", - "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/open-atmos/PyPartMC/blob/main/examples/additive_coag_comparison.ipynb) \n", - "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/open-atmos/PyPartMC.git/main?urlpath=lab/tree/examples/additive_coag_comparison.ipynb)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "159edeb4", - "metadata": {}, - "outputs": [], - "source": [ - "# This file is a part of PyPartMC licensed under the GNU General Public License v3\n", - "# Copyright (C) 2025 University of Illinois Urbana-Champaign\n", - "#\n", - "# PartMC Authors:\n", - "# - https://github.com/compdyn/partmc/graphs/contributors\n", - "# - https://github.com/open-atmos/PyPartMC/graphs/contributors" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "749c1483", - "metadata": {}, - "outputs": [], - "source": [ - "# pylint: disable=too-few-public-methods, import-outside-toplevel, missing-class-docstring" - ] - }, - { - "cell_type": "markdown", - "id": "681810a3", - "metadata": {}, - "source": [ - "\n", - "This notebook aims to compare the results of three stochastic particle-based models using a coagulation only box model test case.\n", - "The models (PyPartMC, PySDM, and Droplets.jl) model the evolution of the droplet size distribution using the Golovin (additive) collision kernel [Golovin 1963](http://mi.mathnet.ru/dan27630). We have chosen this kernel because it has an analytical solution, also included in this notebook.\n", - "These settings reproduce Figure 2a in section 5.1.4 of [Shima et al. 2009](https://DOI.org/10.1002/qj.441) as a \"Hello World\" for droplet collisional growth.\n", - "[PySDM](https://open-atmos.github.io/PySDM/) and [Droplets.jl](https://github.com/emmacware/droplets.jl) use the Shima et al. 2009 coagulation scheme (Super-Droplet Method or SDM), and PyPartMC uses the Weighted Flow Algorithm (WFA) for droplet coagulation [DeVille et al. 2011](https://DOI.org/10.1016/j.jcp.2011.07.027) \n", - "\n", - "Another goal of this notebook is to provide a framework for additional model comparisons. The structure of the workflow in this notebook was created with this in mind. The model subclasses within the run class standardize the format of the common input and output, creating a format for new models to follow. Although all models represented currently are particle-based models, the format and settings should be generalizable to any method that handles coagulation growth of cloud droplets. An example workflow for models different languages is also included in the notebook with Droplets.jl, a native Julia model." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4f8359c2", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import shutil\n", - "\n", - "if \"google.colab\" in sys.modules:\n", - " !pip --quiet install open-atmos-jupyter-utils\n", - " from open_atmos_jupyter_utils import pip_install_on_colab\n", - " pip_install_on_colab('PyPartMC', 'PySDM')\n", - " \n", - " # install julia if not in system\n", - " if shutil.which('julia') is None:\n", - " !wget -q https://julialang-s3.julialang.org/bin/linux/x64/1.11/julia-1.11.3-linux-x86_64.tar.gz -O /tmp/julia.tar.gz; \\\n", - " tar -x -f /tmp/julia.tar.gz -C /usr/local --strip-components 1; \\\n", - " rm /tmp/julia.tar.gz;" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b494ea6e", - "metadata": {}, - "outputs": [], - "source": [ - "from collections import namedtuple\n", - "from functools import partial\n", - "import urllib\n", - "import json\n", - "import subprocess\n", - "import warnings\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from open_atmos_jupyter_utils.show_anim import show_anim \n", - "# note: model-related imports have been moved to the specific model class cells" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "03708307", - "metadata": {}, - "outputs": [], - "source": [ - "SETTINGS = {\n", - " # Physical parameters\n", - " 'N_PART': 2**16,\n", - " 'VOLUME_M3': 1e6,\n", - " 'DT_SEC': 1.,\n", - " 'NUM_CONC_PER_M3': 2**23,\n", - " 'DIAM_AT_MEAN_VOL_M': 2*30.531e-6,\n", - " 'ADDITIVE_KERNEL_COEFF': 1500,\n", - " \n", - " # Plotting parameters\n", - " 'T_MAX_SEC': 3600,\n", - " 'PLOT_TIME_STEP_SEC': 120,\n", - " 'RADIUS_BIN_EDGES_M': list(np.logspace(np.log10(1e-5), np.log10(5e-3), num=129, endpoint=True))\n", - "}\n", - "\n", - "# Derived parameters\n", - "SETTINGS['N_BINS'] = len(SETTINGS[\"RADIUS_BIN_EDGES_M\"]) - 1\n", - "SETTINGS[\"N_PLOT_STEPS\"] = SETTINGS[\"T_MAX_SEC\"] // SETTINGS[\"PLOT_TIME_STEP_SEC\"]\n", - "assert SETTINGS[\"N_PLOT_STEPS\"] * SETTINGS[\"PLOT_TIME_STEP_SEC\"] == SETTINGS[\"T_MAX_SEC\"]\n", - "\n", - "# Save settings into json file for julia process\n", - "with open('setup.json', 'w', encoding='UTF-8') as f:\n", - " json.dump(SETTINGS, f)\n", - "\n", - "SETTINGS['RADIUS_BIN_EDGES_M'] = np.array(SETTINGS['RADIUS_BIN_EDGES_M'])\n", - "SETTINGS = namedtuple(\"Settings\", SETTINGS.keys())(**SETTINGS) # ensure immutable\n", - "\n", - "# Every model is set up to be a subclass of the Run class\n", - "class Run:\n", - " def __init__(self, settings):\n", - " print(f\"Instantiating {self.__class__.__name__}\")\n", - " self.settings = settings" - ] - }, - { - "cell_type": "markdown", - "id": "26e72d27-d1ea-4840-b7f9-7470e378ce6f", - "metadata": {}, - "source": [ - "### Format for a new model run, search NEW_MODEL for lines to change" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9272286c", - "metadata": {}, - "outputs": [], - "source": [ - "# pylint: disable=unnecessary-pass\n", - "class NewModelRun(Run):\n", - " def __init__(self,settings):\n", - " super().__init__(settings)\n", - " # Import model packages here instead of at the top of the notebook\n", - " # import newmodel as nm\n", - "\n", - " # use the settings dictionary to set up the model parameters\n", - " self.settings = settings\n", - " self.bin_data = np.full(self.settings.N_BINS, fill_value=np.nan)\n", - "\n", - " def __call__(self):\n", - " # Run the model here\n", - " # Output should be a dictionary with the following spectra:\n", - " out = {'Number Concentration (#/m^3/unit ln R)':{}, 'Mass Density (kg/m^3/unit ln R)':{}}\n", - "\n", - " # Use the settings dictionary to output common results \n", - " pass\n", - "\n", - " # Example run\n", - " for step in range(self.settings.N_PLOT_STEPS+1):\n", - " if step != 0:\n", - " # Run model until next output time (step)\n", - " pass\n", - "\n", - " # Calculate and save mass density and number concentration\n", - " mass_density = self.bin_data.copy() #kilograms of water per m^3 per dlnr\n", - " pass\n", - " out['Mass Density (kg/m^3/unit ln R)'][step] = mass_density\n", - "\n", - " number_density = self.bin_data.copy() #number of water droplets per m^3\n", - " pass\n", - " out['Number Concentration (#/m^3/unit ln R)'][step] = number_density\n", - "\n", - " return out" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "092168b8", - "metadata": {}, - "outputs": [], - "source": [ - "class AnalyticalSoln(Run):\n", - " \"\"\"\n", - " Analytical solution for the Golovin kernel taken from PySDM \n", - " \"\"\"\n", - " def __init__(self, settings):\n", - " super().__init__(settings)\n", - "\n", - " from PySDM import Formulae\n", - " from PySDM.dynamics.collisions import collision_kernels \n", - "\n", - " self.formulae = Formulae()\n", - " volume_bins_edges = self.formulae.trivia.volume(settings.RADIUS_BIN_EDGES_M)\n", - " dv, dr = np.diff(volume_bins_edges), np.diff(settings.RADIUS_BIN_EDGES_M)\n", - " self.dv_over_dr = dv / dr\n", - " pdf_v_x = volume_bins_edges[:-1] + dv / 2\n", - " self.pdf_r_x = settings.RADIUS_BIN_EDGES_M[:-1] + dr / 2\n", - " \n", - " self.collision_kernel=collision_kernels.Golovin(b=settings.ADDITIVE_KERNEL_COEFF)\n", - " self.times = np.linspace(\n", - " start=0, stop=settings.T_MAX_SEC,\n", - " num=settings.N_PLOT_STEPS+1\n", - " )\n", - " self.analytical_solution = partial(\n", - " self.collision_kernel.analytic_solution,\n", - " x = pdf_v_x,\n", - " x_0= self.formulae.trivia.volume(settings.DIAM_AT_MEAN_VOL_M / 2),\n", - " N_0=self.settings.NUM_CONC_PER_M3\n", - " )\n", - "\n", - " def __call__(self): \n", - " from PySDM.physics import si\n", - "\n", - " out = {'Number Concentration (#/m^3/unit ln R)':{}, 'Mass Density (kg/m^3/unit ln R)':{}}\n", - "\n", - " for step in range(self.settings.N_PLOT_STEPS+1):\n", - " t = step*self.settings.PLOT_TIME_STEP_SEC or 1e-10 #For 0 time\n", - "\n", - " pdf_v_y = (\n", - " self.settings.NUM_CONC_PER_M3\n", - " * self.settings.VOLUME_M3\n", - " * self.analytical_solution(t=t)\n", - " )\n", - " pdf_r_y = pdf_v_y * self.dv_over_dr * self.pdf_r_x\n", - " y_true_mass_density = (\n", - " pdf_r_y\n", - " * self.formulae.trivia.volume(radius=self.pdf_r_x)\n", - " * self.formulae.constants.rho_w\n", - " / self.settings.VOLUME_M3*si.metres**3\n", - " * si.kilograms\n", - " )\n", - " y_true_number_density = (\n", - " pdf_r_y\n", - " / self.settings.VOLUME_M3*si.metres**3\n", - " )\n", - " out['Mass Density (kg/m^3/unit ln R)'][step] = y_true_mass_density\n", - " out['Number Concentration (#/m^3/unit ln R)'][step] = y_true_number_density\n", - "\n", - " return out" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "29491329", - "metadata": {}, - "outputs": [], - "source": [ - "class RunPySDM(Run):\n", - " def __init__(self, settings):\n", - " import PySDM\n", - " from PySDM.physics import si\n", - "\n", - " builder = PySDM.Builder(\n", - " settings.N_PART,\n", - " backend=PySDM.backends.CPU(),\n", - " environment=PySDM.environments.Box(\n", - " dt=settings.DT_SEC * si.s,\n", - " dv=settings.VOLUME_M3 * si.m**3\n", - " )\n", - " )\n", - " trivia = builder.formulae.trivia\n", - " spectrum = PySDM.initialisation.spectra.Exponential(\n", - " norm_factor=settings.NUM_CONC_PER_M3 * settings.VOLUME_M3 / si.m**3,\n", - " scale=trivia.volume(radius=settings.DIAM_AT_MEAN_VOL_M / 2 * si.m)\n", - " )\n", - " builder.add_dynamic(PySDM.dynamics.Coalescence(\n", - " collision_kernel=PySDM.dynamics.collisions.collision_kernels.Golovin(\n", - " b=settings.ADDITIVE_KERNEL_COEFF\n", - " ), adaptive=False\n", - " ))\n", - "\n", - " self.particulator = builder.build(\n", - " attributes=builder.particulator.environment.init_attributes(\n", - " spectral_discretisation=PySDM.initialisation.sampling.spectral_sampling.Logarithmic(\n", - " spectrum,\n", - " )\n", - " ),\n", - " products=(\n", - " PySDM.products.ParticleSizeSpectrumPerVolume(\n", - " radius_bins_edges=settings.RADIUS_BIN_EDGES_M,\n", - " name='Number Concentration (#/m^3/m)',\n", - " ),\n", - " PySDM.products.ParticleVolumeVersusRadiusLogarithmSpectrum(\n", - " radius_bins_edges=settings.RADIUS_BIN_EDGES_M,\n", - " name='Volume Density (1/ unit ln R)',\n", - " )\n", - " )\n", - " )\n", - " super().__init__(settings)\n", - "\n", - " def __call__(self):\n", - " from PySDM.physics import si, constants_defaults\n", - "\n", - " pysdm_output = {'Number Concentration (#/m^3/unit ln R)':{},\n", - " 'Mass Density (kg/m^3/unit ln R)':{}}\n", - " for step in range(self.settings.N_PLOT_STEPS + 1):\n", - " if step != 0:\n", - " self.particulator.run(int(self.settings.PLOT_TIME_STEP_SEC // self.settings.DT_SEC))\n", - " #next two lines convert from dr to dlnr\n", - " pysdm_output['Number Concentration (#/m^3/unit ln R)'][step] = (\n", - " self.particulator.products['Number Concentration (#/m^3/m)'].get() \n", - " * (self.settings.RADIUS_BIN_EDGES_M[1:]-self.settings.RADIUS_BIN_EDGES_M[:-1])\n", - " / (np.log(self.settings.RADIUS_BIN_EDGES_M[1:]) \n", - " - np.log(self.settings.RADIUS_BIN_EDGES_M[:-1]))\n", - " )\n", - " pysdm_output['Mass Density (kg/m^3/unit ln R)'][step] = (\n", - " self.particulator.products['Volume Density (1/ unit ln R)'].get()[0]\n", - " * constants_defaults.rho_w * si.kilogram / si.metre**3\n", - " )\n", - " return pysdm_output" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "80e1dcdd", - "metadata": {}, - "outputs": [], - "source": [ - "class RunPartMC(Run):\n", - " def __init__(self, settings):\n", - " import PyPartMC as ppmc\n", - " gas_data = ppmc.GasData((\"Background\",\"NO2\"))\n", - " env_state = ppmc.EnvState(\n", - " {\n", - " \"rel_humidity\": .99,\n", - " \"latitude\": 40,\n", - " \"longitude\": 0,\n", - " \"altitude\": 0 * ppmc.si.m,\n", - " \"start_time\": 0 * ppmc.si.s,\n", - " \"start_day\": 1,\n", - " }\n", - " )\n", - " env_state.additive_kernel_coefficient = settings.ADDITIVE_KERNEL_COEFF\n", - " assert env_state.additive_kernel_coefficient == settings.ADDITIVE_KERNEL_COEFF\n", - "\n", - " aero_data = ppmc.AeroData((\n", - " {\"H2O\": [1000 * ppmc.si.kg / ppmc.si.m**3, 0, 18.0 * ppmc.si.g / ppmc.si.mol, 0.00]},\n", - " ))\n", - " self.gas_state = ppmc.GasState(gas_data)\n", - "\n", - " common = [\n", - " {\"time\": [0 * ppmc.si.s]},\n", - " {\"rate\": [0 / ppmc.si.s]}\n", - " ]\n", - " placeholder_gas = [\n", - " *common,\n", - " {\"dist\":\n", - " [[{\"placeholder\": {\n", - " \"mass_frac\": [{\"H2O\": [1]}],\n", - " \"diam_type\": \"geometric\",\n", - " \"mode_type\": \"log_normal\",\n", - " \"num_conc\": 0 / ppmc.si.m**3,\n", - " \"geom_mean_diam\": 0.02 * ppmc.si.um,\n", - " \"log10_geom_std_dev\": 0.161,\n", - " },}]]},\n", - " ]\n", - "\n", - " scenario = ppmc.Scenario(\n", - " gas_data,\n", - " aero_data,\n", - " {\n", - " \"temp_profile\": [{\"time\": [0]}, {\"temp\": [300]}],\n", - " \"pressure_profile\": [\n", - " {\"time\": [0]},\n", - " {\"pressure\": [100000]},\n", - " ],\n", - " \"height_profile\": [{\"time\": [0]}, {\"height\": [1000]}],\n", - " \"gas_emissions\": common,\n", - " \"gas_background\": common,\n", - " \"aero_emissions\": placeholder_gas,\n", - " \"aero_background\":placeholder_gas,\n", - " \"loss_function\": \"none\",\n", - " },\n", - " )\n", - "\n", - " scenario.init_env_state(env_state, 0.0)\n", - " aero_dist_init = ppmc.AeroDist(aero_data, [\n", - " {\n", - " \"init1\": {\n", - " \"mass_frac\": [{\"H2O\": [1]}],\n", - " \"diam_type\": \"geometric\",\n", - " \"mode_type\": \"exp\",\n", - " \"num_conc\": settings.NUM_CONC_PER_M3 / ppmc.si.m**3,\n", - " \"diam_at_mean_vol\": settings.DIAM_AT_MEAN_VOL_M * ppmc.si.m,\n", - " },\n", - " }\n", - " ])\n", - "\n", - " run_part_opt = ppmc.RunPartOpt(\n", - " {\n", - " \"output_prefix\": 'additive',\n", - " \"do_coagulation\": True,\n", - " \"coag_kernel\": \"additive\",\n", - " \"t_max\": settings.T_MAX_SEC * ppmc.si.s,\n", - " \"del_t\": settings.DT_SEC * ppmc.si.s,\n", - " }\n", - " )\n", - "\n", - " self.aero_state = ppmc.AeroState(aero_data, settings.N_PART, \"flat\")\n", - " self.aero_state.dist_sample(aero_dist_init)\n", - "\n", - " self.run_part_args = (\n", - " scenario,\n", - " env_state,\n", - " aero_data,\n", - " self.aero_state,\n", - " gas_data,\n", - " self.gas_state,\n", - " run_part_opt,\n", - " ppmc.CampCore(),\n", - " ppmc.Photolysis(),\n", - " )\n", - " self.rad_grid = ppmc.BinGrid(\n", - " len(settings.RADIUS_BIN_EDGES_M) - 1,\n", - " \"log\",\n", - " settings.RADIUS_BIN_EDGES_M[0],\n", - " settings.RADIUS_BIN_EDGES_M[-1]\n", - " )\n", - " super().__init__(settings)\n", - " \n", - " def __call__(self):\n", - " import PyPartMC as ppmc\n", - "\n", - " dists = []\n", - " diameters = self.aero_state.diameters()\n", - " num_concs = self.aero_state.num_concs\n", - " masses = self.aero_state.masses()\n", - " mass_concs = np.array(num_concs) * np.array(masses)\n", - " dists_mass = []\n", - " dists.append(ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, num_concs))\n", - " dists_mass.append(\n", - " ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, mass_concs)\n", - " )\n", - "\n", - " last_output_time = 0.\n", - " last_progress_time = 0.\n", - " i_output = 1\n", - " t_initial = 0\n", - " for i_time in range(1, int(self.settings.T_MAX_SEC // self.settings.DT_SEC) + 1):\n", - " (last_output_time, last_progress_time, i_output) = ppmc.run_part_timestep(\n", - " *self.run_part_args,\n", - " i_time,\n", - " t_initial,\n", - " last_output_time,\n", - " last_progress_time,\n", - " i_output\n", - " )\n", - " \n", - " if np.mod(i_time * self.settings.DT_SEC, self.settings.PLOT_TIME_STEP_SEC) == 0:\n", - " diameters = self.aero_state.diameters()\n", - " num_concs = self.aero_state.num_concs\n", - " masses = self.aero_state.masses()\n", - " mass_concs = np.array(num_concs) * np.array(masses)\n", - " dists.append(ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, num_concs))\n", - " dists_mass.append(\n", - " ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, mass_concs)\n", - " )\n", - "\n", - " return {'Number Concentration (#/m^3/unit ln R)': dists,\n", - " 'Mass Density (kg/m^3/unit ln R)': dists_mass}" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f7d54f29", - "metadata": {}, - "outputs": [], - "source": [ - "class RunDropletsJL(Run):\n", - " def __init__(self, _):\n", - " BASE_URL = (\n", - " 'https://raw.githubusercontent.com/emmacware/droplets.jl/REVISION/'\n", - " ).replace('REVISION', 'c3b5ae4edf12c7b10dc8759639f18de1267bc52b')\n", - " for path in (\n", - " 'src/SDfunc/constants.jl', 'src/SDfunc/binning.jl', 'src/SDfunc/coalescence.jl',\n", - " ):\n", - " with open('Droplets.jl-' + path.replace('/','-'), 'w', encoding='utf-8') as fout:\n", - " with urllib.request.urlopen(BASE_URL + path) as fin:\n", - " fout.write(fin.read().decode('utf-8'))\n", - " \n", - " code_to_write = \"\"\"\n", - " using Pkg\n", - " Pkg.add([\"Combinatorics\", \"Distributions\", \"Random\", \"JSON\", \"Interpolations\"])\n", - " using Random,Combinatorics,Distributions\n", - " include(\"Droplets.jl-src-SDfunc-constants.jl\")\n", - " include(\"Droplets.jl-src-SDfunc-coalescence.jl\")\n", - " include(\"Droplets.jl-src-SDfunc-binning.jl\")\n", - " using JSON\n", - " \n", - " FT = Float64\n", - " setup = JSON.parsefile(\"./setup.json\")\n", - "\n", - " coagsetting_args = (\n", - " Ns = Int(setup[\"N_PART\"]),\n", - " Δt = setup[\"DT_SEC\"],\n", - " ΔV = setup[\"VOLUME_M3\"],\n", - " golovin_kernel_coeff = FT(setup[\"ADDITIVE_KERNEL_COEFF\"]),\n", - " n0 = FT(setup[\"NUM_CONC_PER_M3\"]),\n", - " R0 = FT(setup[\"DIAM_AT_MEAN_VOL_M\"]) / 2\n", - " )\n", - "\n", - " runsetting_args = (\n", - " num_bins = Int(setup[\"N_BINS\"]),\n", - " radius_bins_edges = setup[\"RADIUS_BIN_EDGES_M\"],\n", - " smooth = false,\n", - " output_steps = collect(0:setup[\"PLOT_TIME_STEP_SEC\"]:setup[\"T_MAX_SEC\"]),\n", - " scheme = none(), # Adaptive() not used\n", - " init_method = init_logarithmic\n", - " )\n", - "\n", - " coagsettings = coag_settings{FT}(;coagsetting_args...)\n", - " runsettings =run_settings{FT}(;runsetting_args...,binning_method = none())\n", - " numdens_set =run_settings{FT}(;runsetting_args...,binning_method = number_density)\n", - " massdens_set =run_settings{FT}(;runsetting_args...,binning_method = mass_density_lnr)\n", - "\n", - "\n", - " droplets = runsettings.init_method(coagsettings)\n", - " num_dens_bin::Matrix{FT} = zeros(FT, runsettings.num_bins, length(runsettings.output_steps))\n", - " mass_dens_bin::Matrix{FT} = zeros(FT, runsettings.num_bins, length(runsettings.output_steps))\n", - "\n", - " coag_data = coagulation_run{FT}(coagsettings.Ns)\n", - " for i in 1:length(runsettings.output_steps)\n", - " if i !=1\n", - " timestepper = (runsettings.output_steps[i]-runsettings.output_steps[i-1])/coagsettings.Δt\n", - " for _ in 1:timestepper\n", - " coalescence_timestep!(Serial(),runsettings.scheme,droplets,coag_data,coagsettings)\n", - " end\n", - " end\n", - " num_dens_bin[:,i] = binning_func(droplets,runsettings.output_steps[i],numdens_set,coagsettings)\n", - " mass_dens_bin[:,i] = binning_func(droplets,runsettings.output_steps[i],massdens_set,coagsettings)\n", - " end\n", - "\n", - " dict = Dict()\n", - " dict[\"Number Concentration (#/m^3/unit ln R)\"] = num_dens_bin\n", - " dict[\"Mass Density (kg/m^3/unit ln R)\"] = mass_dens_bin\n", - " json_string = JSON.json(dict)\n", - "\n", - " open(\"output.json\", \"w\") do file\n", - " write(file, json_string)\n", - " end\n", - " \"\"\"\n", - "\n", - " with open('script.jl', 'w', encoding='utf-8') as file:\n", - " file.write(code_to_write)\n", - "\n", - " super().__init__(SETTINGS)\n", - " \n", - " def __call__(self):\n", - " subprocess.run([\"julia\", \"--quiet\", \"script.jl\"], check=True)\n", - " with open('output.json', 'r', encoding='utf8') as file:\n", - " Droplets = json.load(file)\n", - " return Droplets" - ] - }, - { - "cell_type": "markdown", - "id": "7436416c-7525-44f8-82d8-544a9e16039a", - "metadata": {}, - "source": [ - "### Create instances of the models with common setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b33d3ccb", - "metadata": {}, - "outputs": [], - "source": [ - "models = {\n", - " 'analytical': AnalyticalSoln(SETTINGS),\n", - " 'PartMC': RunPartMC(SETTINGS),\n", - " 'PySDM': RunPySDM(SETTINGS),\n", - " '': NewModelRun(SETTINGS) #NEW_MODEL\n", - "}\n", - "if shutil.which('julia') is not None: # pylint: disable=undefined-variable\n", - " models['Droplets.jl'] = RunDropletsJL(SETTINGS)\n", - "else:\n", - " warnings.warn('Julia not found, skipping Droplets.jl run')\n", - "output = {k: print(f\"Running {k}...\") or model() for k, model in models.items()}" - ] - }, - { - "cell_type": "markdown", - "id": "c1d3a943-4523-4364-aa37-603dd2a45d6e", - "metadata": {}, - "source": [ - "### use open_atmos_jupyter_utils to create comparison animation" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "6cc84269", - "metadata": {}, - "outputs": [ + "cells": [ + { + "cell_type": "markdown", + "id": "1a90349d", + "metadata": { + "id": "1a90349d" + }, + "source": [ + "[![nbviewer](https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg)](https://nbviewer.jupyter.org/github/open-atmos/PyPartMC/blob/main/examples/additive_coag_comparison.ipynb) \n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/open-atmos/PyPartMC/blob/main/examples/additive_coag_comparison.ipynb) \n", + "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/open-atmos/PyPartMC.git/main?urlpath=lab/tree/examples/additive_coag_comparison.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "159edeb4", + "metadata": { + "id": "159edeb4" + }, + "outputs": [], + "source": [ + "# This file is a part of PyPartMC licensed under the GNU General Public License v3\n", + "# Copyright (C) 2025 University of Illinois Urbana-Champaign\n", + "#\n", + "# PartMC Authors:\n", + "# - https://github.com/compdyn/partmc/graphs/contributors\n", + "# - https://github.com/open-atmos/PyPartMC/graphs/contributors" + ] + }, { - "data": { - "text/html": [ - "" + "cell_type": "code", + "execution_count": 2, + "id": "749c1483", + "metadata": { + "id": "749c1483" + }, + "outputs": [], + "source": [ + "# pylint: disable=too-few-public-methods, import-outside-toplevel, missing-class-docstring" + ] + }, + { + "cell_type": "markdown", + "id": "681810a3", + "metadata": { + "id": "681810a3" + }, + "source": [ + "\n", + "This notebook aims to compare the results of three stochastic particle-based models using a coagulation only box model test case.\n", + "The models (PyPartMC, PySDM, dustpy, and Droplets.jl) model the evolution of the droplet size distribution using the Golovin (additive) collision kernel [Golovin 1963](http://mi.mathnet.ru/dan27630). We have chosen this kernel because it has an analytical solution, also included in this notebook.\n", + "These settings reproduce Figure 2a in section 5.1.4 of [Shima et al. 2009](https://DOI.org/10.1002/qj.441) as a \"Hello World\" for droplet collisional growth.\n", + "[PySDM](https://open-atmos.github.io/PySDM/) and [Droplets.jl](https://github.com/emmacware/droplets.jl) use the Shima et al. 2009 coagulation scheme (Super-Droplet Method or SDM), and PyPartMC uses the Weighted Flow Algorithm (WFA) for droplet coagulation [DeVille et al. 2011](https://DOI.org/10.1016/j.jcp.2011.07.027).\n", + "The [dustpy](https://iopscience.iop.org/article/10.3847/1538-4357/ac7d58) package is used to simulate dust evolution in protoplanetary disks. It uses a linear distribution sectional approach to simulate coagulation evolution, advantageous for planetary particle size and time scales.\n", + "\n", + "\n", + "Another goal of this notebook is to provide a framework for additional model comparisons. The structure of the workflow in this notebook was created with this in mind. The model subclasses within the run class standardize the format of the common input and output, creating a format for new models to follow. Although all models represented currently are particle-based models, the format and settings should be generalizable to any method that handles coagulation growth of cloud droplets. An example workflow for models different languages is also included in the notebook with Droplets.jl, a native Julia model." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4f8359c2", + "metadata": { + "id": "4f8359c2" + }, + "outputs": [], + "source": [ + "import sys\n", + "import shutil\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " !pip --quiet install open-atmos-jupyter-utils\n", + " from open_atmos_jupyter_utils import pip_install_on_colab\n", + " pip_install_on_colab('PyPartMC', 'PySDM','dustpy')\n", + "\n", + " # install julia if not in system\n", + " if shutil.which('julia') is None:\n", + " !wget -q https://julialang-s3.julialang.org/bin/linux/x64/1.11/julia-1.11.3-linux-x86_64.tar.gz -O /tmp/julia.tar.gz; \\\n", + " tar -x -f /tmp/julia.tar.gz -C /usr/local --strip-components 1; \\\n", + " rm /tmp/julia.tar.gz;" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b494ea6e", + "metadata": { + "id": "b494ea6e" + }, + "outputs": [], + "source": [ + "from collections import namedtuple\n", + "from functools import partial\n", + "import urllib\n", + "import json\n", + "import subprocess\n", + "import warnings\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from open_atmos_jupyter_utils.show_anim import show_anim\n", + "# note: model-related imports have been moved to the specific model class cells" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "03708307", + "metadata": { + "id": "03708307" + }, + "outputs": [], + "source": [ + "SETTINGS = {\n", + " # Physical parameters\n", + " 'N_PART': 2**16,\n", + " 'VOLUME_M3': 1e6,\n", + " 'DT_SEC': 1.,\n", + " 'NUM_CONC_PER_M3': 2**23,\n", + " 'DIAM_AT_MEAN_VOL_M': 2*30.531e-6,\n", + " 'ADDITIVE_KERNEL_COEFF': 1500,\n", + "\n", + " # Plotting parameters\n", + " 'T_MAX_SEC': 3600,\n", + " 'PLOT_TIME_STEP_SEC': 120,\n", + " 'RADIUS_BIN_EDGES_M': list(np.logspace(np.log10(1e-5), np.log10(5e-3), num=129, endpoint=True))\n", + "}\n", + "\n", + "# Derived parameters\n", + "SETTINGS['N_BINS'] = len(SETTINGS[\"RADIUS_BIN_EDGES_M\"]) - 1\n", + "SETTINGS[\"N_PLOT_STEPS\"] = SETTINGS[\"T_MAX_SEC\"] // SETTINGS[\"PLOT_TIME_STEP_SEC\"]\n", + "assert SETTINGS[\"N_PLOT_STEPS\"] * SETTINGS[\"PLOT_TIME_STEP_SEC\"] == SETTINGS[\"T_MAX_SEC\"]\n", + "\n", + "# Save settings into json file for julia process\n", + "with open('setup.json', 'w', encoding='UTF-8') as f:\n", + " json.dump(SETTINGS, f)\n", + "\n", + "SETTINGS['RADIUS_BIN_EDGES_M'] = np.array(SETTINGS['RADIUS_BIN_EDGES_M'])\n", + "SETTINGS = namedtuple(\"Settings\", SETTINGS.keys())(**SETTINGS) # ensure immutable\n", + "\n", + "# Every model is set up to be a subclass of the Run class\n", + "class Run:\n", + " def __init__(self, settings):\n", + " print(f\"Instantiating {self.__class__.__name__}\")\n", + " self.settings = settings" + ] + }, + { + "cell_type": "markdown", + "id": "26e72d27-d1ea-4840-b7f9-7470e378ce6f", + "metadata": { + "id": "26e72d27-d1ea-4840-b7f9-7470e378ce6f" + }, + "source": [ + "### Format for a new model run, search NEW_MODEL for lines to change" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9272286c", + "metadata": { + "id": "9272286c" + }, + "outputs": [], + "source": [ + "# pylint: disable=unnecessary-pass\n", + "class NewModelRun(Run):\n", + " def __init__(self,settings):\n", + " super().__init__(settings)\n", + " # Import model packages here instead of at the top of the notebook\n", + " # import newmodel as nm\n", + "\n", + " # use the settings dictionary to set up the model parameters\n", + " self.settings = settings\n", + " self.bin_data = np.full(self.settings.N_BINS, fill_value=np.nan)\n", + "\n", + " def __call__(self):\n", + " # Run the model here\n", + " # Output should be a dictionary with the following spectra:\n", + " out = {'Number Concentration (#/m^3/unit ln R)':{}, 'Mass Density (kg/m^3/unit ln R)':{}}\n", + "\n", + " # Use the settings dictionary to output common results\n", + " pass\n", + "\n", + " # Example run\n", + " for step in range(self.settings.N_PLOT_STEPS+1):\n", + " if step != 0:\n", + " # Run model until next output time (step)\n", + " pass\n", + "\n", + " # Calculate and save mass density and number concentration\n", + " mass_density = self.bin_data.copy() #kilograms of water per m^3 per dlnr\n", + " pass\n", + " out['Mass Density (kg/m^3/unit ln R)'][step] = mass_density\n", + "\n", + " number_density = self.bin_data.copy() #number of water droplets per m^3\n", + " pass\n", + " out['Number Concentration (#/m^3/unit ln R)'][step] = number_density\n", + "\n", + " return out" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "092168b8", + "metadata": { + "id": "092168b8" + }, + "outputs": [], + "source": [ + "class AnalyticalSoln(Run):\n", + " \"\"\"\n", + " Analytical solution for the Golovin kernel taken from PySDM\n", + " \"\"\"\n", + " def __init__(self, settings):\n", + " super().__init__(settings)\n", + "\n", + " from PySDM import Formulae\n", + " from PySDM.dynamics.collisions import collision_kernels\n", + "\n", + " self.formulae = Formulae()\n", + " volume_bins_edges = self.formulae.trivia.volume(settings.RADIUS_BIN_EDGES_M)\n", + " dv, dr = np.diff(volume_bins_edges), np.diff(settings.RADIUS_BIN_EDGES_M)\n", + " self.dv_over_dr = dv / dr\n", + " pdf_v_x = volume_bins_edges[:-1] + dv / 2\n", + " self.pdf_r_x = settings.RADIUS_BIN_EDGES_M[:-1] + dr / 2\n", + "\n", + " self.collision_kernel=collision_kernels.Golovin(b=settings.ADDITIVE_KERNEL_COEFF)\n", + " self.times = np.linspace(\n", + " start=0, stop=settings.T_MAX_SEC,\n", + " num=settings.N_PLOT_STEPS+1\n", + " )\n", + " self.analytical_solution = partial(\n", + " self.collision_kernel.analytic_solution,\n", + " x = pdf_v_x,\n", + " x_0= self.formulae.trivia.volume(settings.DIAM_AT_MEAN_VOL_M / 2),\n", + " N_0=self.settings.NUM_CONC_PER_M3\n", + " )\n", + "\n", + " def __call__(self):\n", + " from PySDM.physics import si\n", + "\n", + " out = {'Number Concentration (#/m^3/unit ln R)':{}, 'Mass Density (kg/m^3/unit ln R)':{}}\n", + "\n", + " for step in range(self.settings.N_PLOT_STEPS+1):\n", + " t = step*self.settings.PLOT_TIME_STEP_SEC or 1e-10 #For 0 time\n", + "\n", + " pdf_v_y = (\n", + " self.settings.NUM_CONC_PER_M3\n", + " * self.settings.VOLUME_M3\n", + " * self.analytical_solution(t=t)\n", + " )\n", + " pdf_r_y = pdf_v_y * self.dv_over_dr * self.pdf_r_x\n", + " y_true_mass_density = (\n", + " pdf_r_y\n", + " * self.formulae.trivia.volume(radius=self.pdf_r_x)\n", + " * self.formulae.constants.rho_w\n", + " / self.settings.VOLUME_M3*si.metres**3\n", + " * si.kilograms\n", + " )\n", + " y_true_number_density = (\n", + " pdf_r_y\n", + " / self.settings.VOLUME_M3*si.metres**3\n", + " )\n", + " out['Mass Density (kg/m^3/unit ln R)'][step] = y_true_mass_density\n", + " out['Number Concentration (#/m^3/unit ln R)'][step] = y_true_number_density\n", + "\n", + " return out" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "29491329", + "metadata": { + "id": "29491329" + }, + "outputs": [], + "source": [ + "class RunPySDM(Run):\n", + " def __init__(self, settings):\n", + " import PySDM\n", + " from PySDM.physics import si\n", + "\n", + " builder = PySDM.Builder(\n", + " settings.N_PART,\n", + " backend=PySDM.backends.CPU(),\n", + " environment=PySDM.environments.Box(\n", + " dt=settings.DT_SEC * si.s,\n", + " dv=settings.VOLUME_M3 * si.m**3\n", + " )\n", + " )\n", + " trivia = builder.formulae.trivia\n", + " spectrum = PySDM.initialisation.spectra.Exponential(\n", + " norm_factor=settings.NUM_CONC_PER_M3 * settings.VOLUME_M3 / si.m**3,\n", + " scale=trivia.volume(radius=settings.DIAM_AT_MEAN_VOL_M / 2 * si.m)\n", + " )\n", + " builder.add_dynamic(PySDM.dynamics.Coalescence(\n", + " collision_kernel=PySDM.dynamics.collisions.collision_kernels.Golovin(\n", + " b=settings.ADDITIVE_KERNEL_COEFF\n", + " ), adaptive=False\n", + " ))\n", + "\n", + " self.particulator = builder.build(\n", + " attributes=builder.particulator.environment.init_attributes(\n", + " spectral_discretisation=PySDM.initialisation.sampling.spectral_sampling.Logarithmic(\n", + " spectrum,\n", + " )\n", + " ),\n", + " products=(\n", + " PySDM.products.ParticleSizeSpectrumPerVolume(\n", + " radius_bins_edges=settings.RADIUS_BIN_EDGES_M,\n", + " name='Number Concentration (#/m^3/m)',\n", + " ),\n", + " PySDM.products.ParticleVolumeVersusRadiusLogarithmSpectrum(\n", + " radius_bins_edges=settings.RADIUS_BIN_EDGES_M,\n", + " name='Volume Density (1/ unit ln R)',\n", + " )\n", + " )\n", + " )\n", + " super().__init__(settings)\n", + "\n", + " def __call__(self):\n", + " from PySDM.physics import si, constants_defaults\n", + "\n", + " pysdm_output = {'Number Concentration (#/m^3/unit ln R)':{},\n", + " 'Mass Density (kg/m^3/unit ln R)':{}}\n", + " for step in range(self.settings.N_PLOT_STEPS + 1):\n", + " if step != 0:\n", + " self.particulator.run(int(self.settings.PLOT_TIME_STEP_SEC // self.settings.DT_SEC))\n", + " #next two lines convert from dr to dlnr\n", + " pysdm_output['Number Concentration (#/m^3/unit ln R)'][step] = (\n", + " self.particulator.products['Number Concentration (#/m^3/m)'].get()\n", + " * (self.settings.RADIUS_BIN_EDGES_M[1:]-self.settings.RADIUS_BIN_EDGES_M[:-1])\n", + " / (np.log(self.settings.RADIUS_BIN_EDGES_M[1:])\n", + " - np.log(self.settings.RADIUS_BIN_EDGES_M[:-1]))\n", + " )\n", + " pysdm_output['Mass Density (kg/m^3/unit ln R)'][step] = (\n", + " self.particulator.products['Volume Density (1/ unit ln R)'].get()[0]\n", + " * constants_defaults.rho_w * si.kilogram / si.metre**3\n", + " )\n", + " return pysdm_output" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "80e1dcdd", + "metadata": { + "id": "80e1dcdd" + }, + "outputs": [], + "source": [ + "class RunPartMC(Run):\n", + " def __init__(self, settings):\n", + " import PyPartMC as ppmc\n", + " gas_data = ppmc.GasData((\"Background\",\"NO2\"))\n", + " env_state = ppmc.EnvState(\n", + " {\n", + " \"rel_humidity\": .99,\n", + " \"latitude\": 40,\n", + " \"longitude\": 0,\n", + " \"altitude\": 0 * ppmc.si.m,\n", + " \"start_time\": 0 * ppmc.si.s,\n", + " \"start_day\": 1,\n", + " }\n", + " )\n", + " env_state.additive_kernel_coefficient = settings.ADDITIVE_KERNEL_COEFF\n", + " assert env_state.additive_kernel_coefficient == settings.ADDITIVE_KERNEL_COEFF\n", + "\n", + " aero_data = ppmc.AeroData((\n", + " {\"H2O\": [1000 * ppmc.si.kg / ppmc.si.m**3, 0, 18.0 * ppmc.si.g / ppmc.si.mol, 0.00]},\n", + " ))\n", + " self.gas_state = ppmc.GasState(gas_data)\n", + "\n", + " common = [\n", + " {\"time\": [0 * ppmc.si.s]},\n", + " {\"rate\": [0 / ppmc.si.s]}\n", + " ]\n", + " placeholder_gas = [\n", + " *common,\n", + " {\"dist\":\n", + " [[{\"placeholder\": {\n", + " \"mass_frac\": [{\"H2O\": [1]}],\n", + " \"diam_type\": \"geometric\",\n", + " \"mode_type\": \"log_normal\",\n", + " \"num_conc\": 0 / ppmc.si.m**3,\n", + " \"geom_mean_diam\": 0.02 * ppmc.si.um,\n", + " \"log10_geom_std_dev\": 0.161,\n", + " },}]]},\n", + " ]\n", + "\n", + " scenario = ppmc.Scenario(\n", + " gas_data,\n", + " aero_data,\n", + " {\n", + " \"temp_profile\": [{\"time\": [0]}, {\"temp\": [300]}],\n", + " \"pressure_profile\": [\n", + " {\"time\": [0]},\n", + " {\"pressure\": [100000]},\n", + " ],\n", + " \"height_profile\": [{\"time\": [0]}, {\"height\": [1000]}],\n", + " \"gas_emissions\": common,\n", + " \"gas_background\": common,\n", + " \"aero_emissions\": placeholder_gas,\n", + " \"aero_background\":placeholder_gas,\n", + " \"loss_function\": \"none\",\n", + " },\n", + " )\n", + "\n", + " scenario.init_env_state(env_state, 0.0)\n", + " aero_dist_init = ppmc.AeroDist(aero_data, [\n", + " {\n", + " \"init1\": {\n", + " \"mass_frac\": [{\"H2O\": [1]}],\n", + " \"diam_type\": \"geometric\",\n", + " \"mode_type\": \"exp\",\n", + " \"num_conc\": settings.NUM_CONC_PER_M3 / ppmc.si.m**3,\n", + " \"diam_at_mean_vol\": settings.DIAM_AT_MEAN_VOL_M * ppmc.si.m,\n", + " },\n", + " }\n", + " ])\n", + "\n", + " run_part_opt = ppmc.RunPartOpt(\n", + " {\n", + " \"output_prefix\": 'additive',\n", + " \"do_coagulation\": True,\n", + " \"coag_kernel\": \"additive\",\n", + " \"t_max\": settings.T_MAX_SEC * ppmc.si.s,\n", + " \"del_t\": settings.DT_SEC * ppmc.si.s,\n", + " }\n", + " )\n", + "\n", + " self.aero_state = ppmc.AeroState(aero_data, settings.N_PART, \"flat\")\n", + " self.aero_state.dist_sample(aero_dist_init)\n", + "\n", + " self.run_part_args = (\n", + " scenario,\n", + " env_state,\n", + " aero_data,\n", + " self.aero_state,\n", + " gas_data,\n", + " self.gas_state,\n", + " run_part_opt,\n", + " ppmc.CampCore(),\n", + " ppmc.Photolysis(),\n", + " )\n", + " self.rad_grid = ppmc.BinGrid(\n", + " len(settings.RADIUS_BIN_EDGES_M) - 1,\n", + " \"log\",\n", + " settings.RADIUS_BIN_EDGES_M[0],\n", + " settings.RADIUS_BIN_EDGES_M[-1]\n", + " )\n", + " super().__init__(settings)\n", + "\n", + " def __call__(self):\n", + " import PyPartMC as ppmc\n", + "\n", + " dists = []\n", + " diameters = self.aero_state.diameters()\n", + " num_concs = self.aero_state.num_concs\n", + " masses = self.aero_state.masses()\n", + " mass_concs = np.array(num_concs) * np.array(masses)\n", + " dists_mass = []\n", + " dists.append(ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, num_concs))\n", + " dists_mass.append(\n", + " ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, mass_concs)\n", + " )\n", + "\n", + " last_output_time = 0.\n", + " last_progress_time = 0.\n", + " i_output = 1\n", + " t_initial = 0\n", + " for i_time in range(1, int(self.settings.T_MAX_SEC // self.settings.DT_SEC) + 1):\n", + " (last_output_time, last_progress_time, i_output) = ppmc.run_part_timestep(\n", + " *self.run_part_args,\n", + " i_time,\n", + " t_initial,\n", + " last_output_time,\n", + " last_progress_time,\n", + " i_output\n", + " )\n", + "\n", + " if np.mod(i_time * self.settings.DT_SEC, self.settings.PLOT_TIME_STEP_SEC) == 0:\n", + " diameters = self.aero_state.diameters()\n", + " num_concs = self.aero_state.num_concs\n", + " masses = self.aero_state.masses()\n", + " mass_concs = np.array(num_concs) * np.array(masses)\n", + " dists.append(ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, num_concs))\n", + " dists_mass.append(\n", + " ppmc.histogram_1d(self.rad_grid, np.array(diameters)/2, mass_concs)\n", + " )\n", + "\n", + " return {'Number Concentration (#/m^3/unit ln R)': dists,\n", + " 'Mass Density (kg/m^3/unit ln R)': dists_mass}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f7d54f29", + "metadata": { + "id": "f7d54f29" + }, + "outputs": [], + "source": [ + "class RunDropletsJL(Run):\n", + " def __init__(self, _):\n", + " BASE_URL = (\n", + " 'https://raw.githubusercontent.com/emmacware/droplets.jl/REVISION/'\n", + " ).replace('REVISION', 'c3b5ae4edf12c7b10dc8759639f18de1267bc52b')\n", + " for path in (\n", + " 'src/SDfunc/constants.jl', 'src/SDfunc/binning.jl', 'src/SDfunc/coalescence.jl',\n", + " ):\n", + " with open('Droplets.jl-' + path.replace('/','-'), 'w', encoding='utf-8') as fout:\n", + " with urllib.request.urlopen(BASE_URL + path) as fin:\n", + " fout.write(fin.read().decode('utf-8'))\n", + "\n", + " code_to_write = \"\"\"\n", + " using Pkg\n", + " Pkg.add([\"Combinatorics\", \"Distributions\", \"Random\", \"JSON\", \"Interpolations\"])\n", + " using Random,Combinatorics,Distributions\n", + " include(\"Droplets.jl-src-SDfunc-constants.jl\")\n", + " include(\"Droplets.jl-src-SDfunc-coalescence.jl\")\n", + " include(\"Droplets.jl-src-SDfunc-binning.jl\")\n", + " using JSON\n", + "\n", + " FT = Float64\n", + " setup = JSON.parsefile(\"./setup.json\")\n", + "\n", + " coagsetting_args = (\n", + " Ns = Int(setup[\"N_PART\"]),\n", + " Δt = setup[\"DT_SEC\"],\n", + " ΔV = setup[\"VOLUME_M3\"],\n", + " golovin_kernel_coeff = FT(setup[\"ADDITIVE_KERNEL_COEFF\"]),\n", + " n0 = FT(setup[\"NUM_CONC_PER_M3\"]),\n", + " R0 = FT(setup[\"DIAM_AT_MEAN_VOL_M\"]) / 2\n", + " )\n", + "\n", + " runsetting_args = (\n", + " num_bins = Int(setup[\"N_BINS\"]),\n", + " radius_bins_edges = setup[\"RADIUS_BIN_EDGES_M\"],\n", + " smooth = false,\n", + " output_steps = collect(0:setup[\"PLOT_TIME_STEP_SEC\"]:setup[\"T_MAX_SEC\"]),\n", + " scheme = none(), # Adaptive() not used\n", + " init_method = init_logarithmic\n", + " )\n", + "\n", + " coagsettings = coag_settings{FT}(;coagsetting_args...)\n", + " runsettings =run_settings{FT}(;runsetting_args...,binning_method = none())\n", + " numdens_set =run_settings{FT}(;runsetting_args...,binning_method = number_density)\n", + " massdens_set =run_settings{FT}(;runsetting_args...,binning_method = mass_density_lnr)\n", + "\n", + "\n", + " droplets = runsettings.init_method(coagsettings)\n", + " num_dens_bin::Matrix{FT} = zeros(FT, runsettings.num_bins, length(runsettings.output_steps))\n", + " mass_dens_bin::Matrix{FT} = zeros(FT, runsettings.num_bins, length(runsettings.output_steps))\n", + "\n", + " coag_data = coagulation_run{FT}(coagsettings.Ns)\n", + " for i in 1:length(runsettings.output_steps)\n", + " if i !=1\n", + " timestepper = (runsettings.output_steps[i]-runsettings.output_steps[i-1])/coagsettings.Δt\n", + " for _ in 1:timestepper\n", + " coalescence_timestep!(Serial(),runsettings.scheme,droplets,coag_data,coagsettings)\n", + " end\n", + " end\n", + " num_dens_bin[:,i] = binning_func(droplets,runsettings.output_steps[i],numdens_set,coagsettings)\n", + " mass_dens_bin[:,i] = binning_func(droplets,runsettings.output_steps[i],massdens_set,coagsettings)\n", + " end\n", + "\n", + " dict = Dict()\n", + " dict[\"Number Concentration (#/m^3/unit ln R)\"] = num_dens_bin\n", + " dict[\"Mass Density (kg/m^3/unit ln R)\"] = mass_dens_bin\n", + " json_string = JSON.json(dict)\n", + "\n", + " open(\"output.json\", \"w\") do file\n", + " write(file, json_string)\n", + " end\n", + " \"\"\"\n", + "\n", + " with open('script.jl', 'w', encoding='utf-8') as file:\n", + " file.write(code_to_write)\n", + "\n", + " super().__init__(SETTINGS)\n", + "\n", + " def __call__(self):\n", + " subprocess.run([\"julia\", \"--quiet\", \"script.jl\"], check=True)\n", + " with open('output.json', 'r', encoding='utf8') as file:\n", + " Droplets = json.load(file)\n", + " return Droplets" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "AyZyoU2CmJs4", + "metadata": { + "id": "AyZyoU2CmJs4" + }, + "outputs": [], + "source": [ + "class DustPy(Run):\n", + " def __init__(self,settings):\n", + " super().__init__(settings)\n", + " from dustpy import Simulation\n", + " #DUSTPY USES CGS UNITS AND COLUMN AVERAGE DENSITIES, INPUT AND OUTPUT ARE SI\n", + " cm = 1.e2\n", + " g_per_cm3 = 1.\n", + "\n", + " m0 = 4/3 * np.pi * ((settings.DIAM_AT_MEAN_VOL_M*cm)/2)**3 * g_per_cm3 # ->cm, ->g\n", + " #kernel coefficient, changing to integrated column\n", + " self.a = settings.ADDITIVE_KERNEL_COEFF/(\n", + " settings.VOLUME_M3*m0*settings.NUM_CONC_PER_M3/cm**2)\n", + "\n", + " #Initialize\n", + " sim = Simulation()\n", + " sim.ini.grid.Nr = 3\n", + " numberpermassdecade = 60\n", + " sim.ini.grid.Nmbpd = numberpermassdecade\n", + " sim.ini.grid.mmin = 4/3 * np.pi * (\n", + " settings.RADIUS_BIN_EDGES_M[0]*1e2)**3 *g_per_cm3#m0\n", + " sim.ini.grid.mmax = 4/3 * np.pi * (\n", + " settings.RADIUS_BIN_EDGES_M[-1]*1e2)**3 *g_per_cm3 # ->cm,g\n", + " sim.ini.dust.allowDriftingParticles = True\n", + " sim.initialize()\n", + "\n", + " sim.dust.SigmaFloor[1, ...] = 0.\n", + " #These are water droplets, not planet dust!\n", + " sim.dust.rhos = g_per_cm3\n", + "\n", + " #Only Coagulation\n", + " del sim.integrator.instructions[1]\n", + " # Turning off gas source to not influence the time stepping\n", + " sim.gas.S.tot[...] = 0.\n", + " sim.gas.S.tot.updater = None\n", + " # Turning off dust advection\n", + " sim.dust.v.rad[...] = 0.\n", + " sim.dust.v.rad.updater = None\n", + " # Turning off fragmentation. Only sticking is considered\n", + " sim.dust.p.frag[...] = 0.\n", + " sim.dust.p.frag.updater = None\n", + " sim.dust.p.stick[...] = 1.\n", + " sim.dust.p.stick.updater = None\n", + " # Setting the constant kernel\n", + " sim.dust.kernel[...] = self.a * (sim.grid.m[:, None] + sim.grid.m[None, :])[None, ...]\n", + " sim.dust.kernel.updater = None\n", + " # Setting the initial dust surface density\n", + " sim.dust.Sigma[...] = sim.dust.SigmaFloor[...]\n", + "\n", + " m = sim.grid.m\n", + " A = np.mean(m[1:]/m[:-1])\n", + " B = 2 * (A-1) / (A+1)\n", + " sim.t = 1\n", + " #Exponential Distribution\n", + " # pylint: disable=invalid-unary-operand-type\n", + " sim.dust.Sigma[1, ...] = np.exp(-m/m0)/m0 *(B*m)*m*(settings.NUM_CONC_PER_M3/cm**2) #cm-2\n", + "\n", + " # Updating the simulation object\n", + " sim.update()\n", + "\n", + " plot_times = np.linspace(\n", + " start=settings.PLOT_TIME_STEP_SEC, stop=settings.T_MAX_SEC,\n", + " num=settings.N_PLOT_STEPS\n", + " )\n", + " snapshots = np.hstack(\n", + " [sim.t, plot_times]\n", + " )\n", + " sim.t.snapshots = snapshots\n", + " sim.writer.datadir = \"dustpy_\"\n", + " sim.writer.overwrite = True\n", + "\n", + " #for use during __call\n", + " self.sim = sim\n", + " self.settings = settings\n", + "\n", + "\n", + " def __call__(self):\n", + "\n", + " out = {'Number Concentration (#/m^3/unit ln R)':{}, 'Mass Density (kg/m^3/unit ln R)':{}}\n", + " #Conversions from cgs to si\n", + " kg = 1e-3\n", + " meter = 1e-2\n", + "\n", + " #run Simulation\n", + " self.sim.run()\n", + "\n", + " SigmaLinearHighRes = self.sim.writer.read.sequence(\n", + " \"dust.Sigma\")*kg/meter**2 #only 2 because cm2\n", + " mHighRes = self.sim.writer.read.sequence(\"grid.m\")*kg\n", + " aHighRes = self.sim.writer.read.sequence(\"dust.a\")[0,1,:]*meter\n", + "\n", + " for step in range(self.settings.N_PLOT_STEPS+1):\n", + " mass_density = np.full(self.settings.N_BINS, fill_value=0.)\n", + " number_density = np.full(self.settings.N_BINS, fill_value=0.)\n", + "\n", + " # bin data to settings.RADIUS_BIN_EDGES_M\n", + " k = 0\n", + " for i,radius in enumerate(aHighRes):\n", + " for j in range(k,len(self.settings.RADIUS_BIN_EDGES_M)-1):\n", + " if self.settings.RADIUS_BIN_EDGES_M[j] <= radius < (\n", + " self.settings.RADIUS_BIN_EDGES_M[j + 1]):\n", + " mass_density[j] += SigmaLinearHighRes[step, 1, i]\n", + " number_density[j] += SigmaLinearHighRes[step, 1, i]/mHighRes[1, i]\n", + " k = j\n", + " break\n", + " dlnr = np.log(self.settings.RADIUS_BIN_EDGES_M[1:])-np.log(\n", + " self.settings.RADIUS_BIN_EDGES_M[:-1])\n", + "\n", + " out['Mass Density (kg/m^3/unit ln R)'][step] = mass_density/dlnr\n", + " out['Number Concentration (#/m^3/unit ln R)'][step] = number_density/dlnr\n", + "\n", + " return out" + ] + }, + { + "cell_type": "markdown", + "id": "7436416c-7525-44f8-82d8-544a9e16039a", + "metadata": { + "id": "7436416c-7525-44f8-82d8-544a9e16039a" + }, + "source": [ + "### Create instances of the models with common setup" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b33d3ccb", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "b33d3ccb", + "outputId": "3ee99629-6f11-415e-8a65-aa5925f05610" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Instantiating AnalyticalSoln\n", + "Instantiating RunPartMC\n", + "Instantiating RunPySDM\n", + "Instantiating DustPy\n", + "Instantiating NewModelRun\n", + "Instantiating RunDropletsJL\n", + "Running analytical...\n", + "Running PartMC...\n", + "Running PySDM...\n", + "Running DustPy...\n", + "\n", + "DustPy v1.0.8\n", + "\n", + "Documentation: https://stammler.github.io/dustpy/\n", + "PyPI: https://pypi.org/project/dustpy/\n", + "GitHub: https://github.com/stammler/dustpy/\n", + "\u001b[94m\n", + "Please cite Stammler & Birnstiel (2022).\u001b[0m\n", + "\n", + "\u001b[93mChecking for mass conservation...\n", + "\u001b[0m\n", + "\u001b[93m - Sticking:\u001b[0m\n", + "\u001b[0m max. rel. error: \u001b[92m 8.88e-14\u001b[0m\n", + " for particle collision\n", + " m[439] = 1.60e-02 g with\n", + " m[534] = 4.26e-01 g\u001b[0m\n", + "\u001b[93m - Full fragmentation:\u001b[0m\n", + "\u001b[0m max. rel. error: \u001b[92m 2.00e-15\u001b[0m\n", + " for particle collision\n", + " m[381] = 2.16e-03 g with\n", + " m[438] = 1.55e-02 g\u001b[0m\n", + "\u001b[93m - Erosion:\u001b[0m\n", + "\u001b[0m max. rel. error: \u001b[92m 3.44e-15\u001b[0m\n", + " for particle collision\n", + " m[436] = 1.44e-02 g with\n", + " m[535] = 4.41e-01 g\n", + "\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0000.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0001.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0002.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0003.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0004.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0005.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0006.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0007.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0008.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0009.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0010.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0011.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0012.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0013.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0014.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0015.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0016.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0017.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0018.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0019.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0020.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0021.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0022.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0023.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0024.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0025.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0026.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0027.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0028.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0029.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Writing file \u001b[94mdustpy_/data0030.hdf5\u001b[0m\n", + "Writing dump file \u001b[94mdustpy_/frame.dmp\u001b[0m\n", + "Execution time: \u001b[94m0:00:49\u001b[0m\n", + "Running ...\n", + "Running Droplets.jl...\n" + ] + } ], - "text/plain": [ - "" + "source": [ + "models = {\n", + " 'analytical': AnalyticalSoln(SETTINGS),\n", + " 'PartMC': RunPartMC(SETTINGS),\n", + " 'PySDM': RunPySDM(SETTINGS),\n", + " 'DustPy': DustPy(SETTINGS),\n", + " '': NewModelRun(SETTINGS) #NEW_MODEL\n", + "}\n", + "if shutil.which('julia') is not None: # pylint: disable=undefined-variable\n", + " models['Droplets.jl'] = RunDropletsJL(SETTINGS)\n", + "else:\n", + " warnings.warn('Julia not found, skipping Droplets.jl run')\n", + "output = {k: print(f\"Running {k}...\") or model() for k, model in models.items()}" ] - }, - "metadata": {}, - "output_type": "display_data" }, { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "afd169224d2540a0a170acb9ef676fac", - "version_major": 2, - "version_minor": 0 + "cell_type": "markdown", + "id": "c1d3a943-4523-4364-aa37-603dd2a45d6e", + "metadata": { + "id": "c1d3a943-4523-4364-aa37-603dd2a45d6e" }, - "text/plain": [ - "HTML(value=\"./tmpl0kmjlg7.gif
\")" + "source": [ + "### use open_atmos_jupyter_utils to create comparison animation" ] - }, - "metadata": {}, - "output_type": "display_data" }, { - "data": { - "text/html": [ - "" + "cell_type": "code", + "execution_count": 13, + "id": "6cc84269", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "a70a8f1547324003bf5060e4a84ad1b7", + "2790c4f59c444e559a9eafc6d0efcf78", + "8bfad5ba5afa473ebf40426240c7d291", + "e491d2d597bf42218d52dd3e2d86fb64", + "66ea2bf94eb64790b2b53a0a7d28d1d0", + "dd3e2f8ce6f04a808e16ec6da1176ffc" + ] + }, + "id": "6cc84269", + "outputId": "dc996ce6-b3ed-4278-9968-432c41ac8947" + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a70a8f1547324003bf5060e4a84ad1b7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='tmpfo29mctg.gif', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e491d2d597bf42218d52dd3e2d86fb64", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='tmpp1j5x9dr.gif', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + } ], - "text/plain": [ - "" + "source": [ + "t_steps = np.divide(\n", + " np.array(range(0, int(SETTINGS.T_MAX_SEC+1), 1200)),\n", + " SETTINGS.PLOT_TIME_STEP_SEC\n", + ").astype(int)\n", + "radius_bins = np.array(models['PartMC'].rad_grid.centers)\n", + "kilograms_to_grams = 1e3\n", + "\n", + "markers = {\n", + " 'PartMC':'o',\n", + " 'PySDM':'x',\n", + " 'DustPy':'s',\n", + " 'Droplets.jl':'^',\n", + " '':'s' #NEW_MODEL\n", + "}\n", + "\n", + "class AnimFunc:\n", + " def __init__(self, binning_method):\n", + " self.binning_method = binning_method\n", + "\n", + " def __call__(self, frame):\n", + " if self.binning_method == 'Mass Density (kg/m^3/unit ln R)':\n", + " unit_adjustment = kilograms_to_grams\n", + " else:\n", + " unit_adjustment = 1\n", + "\n", + " if frame > SETTINGS.T_MAX_SEC/SETTINGS.PLOT_TIME_STEP_SEC:\n", + " frame = int(SETTINGS.T_MAX_SEC/SETTINGS.PLOT_TIME_STEP_SEC)\n", + "\n", + " t = list(range(0, int((frame+1)*SETTINGS.PLOT_TIME_STEP_SEC), 1200))\n", + "\n", + " def plot(step, color_analytic, color_monte_carlo, label=False):\n", + " for key in models:\n", + " xy = radius_bins, np.array(output[key][self.binning_method][step])*unit_adjustment\n", + " if key == 'analytical':\n", + " plt.plot(*xy, color=color_analytic, label=key if label else None)\n", + " else:\n", + " plt.plot(*xy, markers[key], ms=3, color=color_monte_carlo,\n", + " label=key if label else None)\n", + "\n", + " for t_sec in t:\n", + " tstep = int(t_sec/SETTINGS.PLOT_TIME_STEP_SEC)\n", + " idx = np.argmax(output['analytical'][self.binning_method][tstep])\n", + " text_height = 1.05*max(output['analytical'][self.binning_method][tstep])\n", + " plt.text(radius_bins[idx], text_height*unit_adjustment,\n", + " f't = {t_sec/60} min', fontsize=8)\n", + " plot(tstep, 'darkgrey', 'darkgrey')\n", + " plot(frame, 'black', None, label=True)\n", + "\n", + " plt.xscale(\"log\")\n", + " plt.xlabel(\"Radius (m)\")\n", + " plt.ylabel(self.binning_method.replace('^3', '$^3$').replace('kg','g'))\n", + " if self.binning_method != 'Number Concentration (#/m^3/unit ln R)':\n", + " plt.ylim([0,2])\n", + " plt.xlim([SETTINGS.RADIUS_BIN_EDGES_M[0],SETTINGS.RADIUS_BIN_EDGES_M[-1]])\n", + " plt.legend(loc='upper right')\n", + " plt.title(\"Time Evolution of Particle Size Distribution, t = \"\n", + " +f\"{frame*SETTINGS.PLOT_TIME_STEP_SEC//60:02d} min\")\n", + " return plt.gcf()\n", + "\n", + "for spectrum_type in output['analytical']:\n", + " show_anim(\n", + " plot_func=AnimFunc(spectrum_type),\n", + " frame_range=range(0, SETTINGS.N_PLOT_STEPS+20, 1),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "a5fe6e01-c736-45a7-87d1-436a72d53100", + "metadata": { + "id": "a5fe6e01-c736-45a7-87d1-436a72d53100" + }, + "source": [ + "### sanity check for conservation of mass" ] - }, - "metadata": {}, - "output_type": "display_data" }, { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "04d5423b58cf434abd60acb4cb72a17d", - "version_major": 2, - "version_minor": 0 + "cell_type": "code", + "execution_count": 14, + "id": "0eaf456b", + "metadata": { + "id": "0eaf456b" }, - "text/plain": [ - "HTML(value=\"./tmpo_g26hgw.gif
\")" + "outputs": [], + "source": [ + "for model in output:\n", + " if model == '':\n", + " continue\n", + " mass_spec = output[model]['Mass Density (kg/m^3/unit ln R)']\n", + " for output_step in range(SETTINGS.N_PLOT_STEPS+1):\n", + " np.testing.assert_approx_equal(\n", + " desired=1e-3, # kilograms\n", + " actual=np.dot(\n", + " mass_spec[output_step],\n", + " np.log(SETTINGS.RADIUS_BIN_EDGES_M[1:]) - np.log(SETTINGS.RADIUS_BIN_EDGES_M[:-1])\n", + " ),\n", + " significant=1.75\n", + " )" ] - }, - "metadata": {}, - "output_type": "display_data" + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fead341e-6092-428f-8dce-3b4660d04526", + "metadata": { + "id": "fead341e-6092-428f-8dce-3b4660d04526" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "2790c4f59c444e559a9eafc6d0efcf78": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "66ea2bf94eb64790b2b53a0a7d28d1d0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8bfad5ba5afa473ebf40426240c7d291": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "button_color": null, + "font_weight": "" + } + }, + "a70a8f1547324003bf5060e4a84ad1b7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ButtonView", + "button_style": "", + "description": "tmpfo29mctg.gif", + "disabled": false, + "icon": "", + "layout": "IPY_MODEL_2790c4f59c444e559a9eafc6d0efcf78", + "style": "IPY_MODEL_8bfad5ba5afa473ebf40426240c7d291", + "tooltip": "" + } + }, + "dd3e2f8ce6f04a808e16ec6da1176ffc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "button_color": null, + "font_weight": "" + } + }, + "e491d2d597bf42218d52dd3e2d86fb64": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ButtonView", + "button_style": "", + "description": "tmpp1j5x9dr.gif", + "disabled": false, + "icon": "", + "layout": "IPY_MODEL_66ea2bf94eb64790b2b53a0a7d28d1d0", + "style": "IPY_MODEL_dd3e2f8ce6f04a808e16ec6da1176ffc", + "tooltip": "" + } + } + } } - ], - "source": [ - "t_steps = np.divide(\n", - " np.array(range(0, int(SETTINGS.T_MAX_SEC+1), 1200)),\n", - " SETTINGS.PLOT_TIME_STEP_SEC\n", - ").astype(int)\n", - "radius_bins = np.array(models['PartMC'].rad_grid.centers)\n", - "kilograms_to_grams = 1e3\n", - "\n", - "markers = {\n", - " 'PartMC':'o', \n", - " 'PySDM':'x',\n", - " 'Droplets.jl':'^',\n", - " '':'s' #NEW_MODEL\n", - "} \n", - "\n", - "class AnimFunc:\n", - " def __init__(self, binning_method):\n", - " self.binning_method = binning_method\n", - "\n", - " def __call__(self, frame):\n", - " if self.binning_method == 'Mass Density (kg/m^3/unit ln R)':\n", - " unit_adjustment = kilograms_to_grams\n", - " else:\n", - " unit_adjustment = 1\n", - "\n", - " if frame > SETTINGS.T_MAX_SEC/SETTINGS.PLOT_TIME_STEP_SEC:\n", - " frame = int(SETTINGS.T_MAX_SEC/SETTINGS.PLOT_TIME_STEP_SEC)\n", - "\n", - " t = list(range(0, int((frame+1)*SETTINGS.PLOT_TIME_STEP_SEC), 1200))\n", - "\n", - " def plot(step, color_analytic, color_monte_carlo, label=False):\n", - " for key in models:\n", - " xy = radius_bins, np.array(output[key][self.binning_method][step])*unit_adjustment\n", - " if key == 'analytical':\n", - " plt.plot(*xy, color=color_analytic, label=key if label else None)\n", - " else:\n", - " plt.plot(*xy, markers[key], ms=3, color=color_monte_carlo, \n", - " label=key if label else None)\n", - "\n", - " for t_sec in t:\n", - " tstep = int(t_sec/SETTINGS.PLOT_TIME_STEP_SEC)\n", - " idx = np.argmax(output['analytical'][self.binning_method][tstep])\n", - " text_height = 1.05*max(output['analytical'][self.binning_method][tstep])\n", - " plt.text(radius_bins[idx], text_height*unit_adjustment, \n", - " f't = {t_sec/60} min', fontsize=8)\n", - " plot(tstep, 'darkgrey', 'darkgrey')\n", - " plot(frame, 'black', None, label=True)\n", - " \n", - " plt.xscale(\"log\")\n", - " plt.xlabel(\"Radius (m)\")\n", - " plt.ylabel(self.binning_method.replace('^3', '$^3$').replace('kg','g'))\n", - " if self.binning_method != 'Number Concentration (#/m^3/unit ln R)':\n", - " plt.ylim([0,2])\n", - " plt.xlim([SETTINGS.RADIUS_BIN_EDGES_M[0],SETTINGS.RADIUS_BIN_EDGES_M[-1]])\n", - " plt.legend(loc='upper right')\n", - " plt.title(\"Time Evolution of Particle Size Distribution, t = \"\n", - " +f\"{frame*SETTINGS.PLOT_TIME_STEP_SEC//60:02d} min\")\n", - " return plt.gcf()\n", - "\n", - "for spectrum_type in output['analytical']:\n", - " show_anim(\n", - " plot_func=AnimFunc(spectrum_type),\n", - " frame_range=range(0, SETTINGS.N_PLOT_STEPS+20, 1),\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "a5fe6e01-c736-45a7-87d1-436a72d53100", - "metadata": {}, - "source": [ - "### sanity check for conservation of mass" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "0eaf456b", - "metadata": {}, - "outputs": [], - "source": [ - "for model in output:\n", - " if model == '':\n", - " continue\n", - " mass_spec = output[model]['Mass Density (kg/m^3/unit ln R)']\n", - " for output_step in range(SETTINGS.N_PLOT_STEPS+1):\n", - " np.testing.assert_approx_equal(\n", - " desired=1e-3, # kilograms\n", - " actual=np.dot(\n", - " mass_spec[output_step],\n", - " np.log(SETTINGS.RADIUS_BIN_EDGES_M[1:]) - np.log(SETTINGS.RADIUS_BIN_EDGES_M[:-1])\n", - " ),\n", - " significant=1.75\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fead341e-6092-428f-8dce-3b4660d04526", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/setup.py b/setup.py index 5a23d98b..5d620837 100644 --- a/setup.py +++ b/setup.py @@ -162,6 +162,7 @@ def build_extension(self, ext): # pylint: disable=too-many-branches "PySDM", "PyMieScatt", "SciPy", + "dustpy", ], }, )