示例#1
0
    def plot(self, vals, t):
        setup = self.setup

        if t == 0:
            analytic_solution = setup.spectrum.size_distribution
        else:
            analytic_solution = lambda x: setup.norm_factor * setup.kernel.analytic_solution(
                x=x, t=t, x_0=setup.X0, N_0=setup.n_part)

        volume_bins_edges = phys.volume(setup.radius_bins_edges)
        dm = np.diff(volume_bins_edges)
        dr = np.diff(setup.radius_bins_edges)

        pdf_m_x = volume_bins_edges[:-1] + dm / 2
        pdf_m_y = analytic_solution(pdf_m_x)

        pdf_r_x = setup.radius_bins_edges[:-1] + dr / 2
        pdf_r_y = pdf_m_y * dm / dr * pdf_r_x

        pyplot.plot(pdf_r_x * si.metres / si.micrometres,
                    pdf_r_y * phys.volume(radius=pdf_r_x) * setup.rho /
                    setup.dv * si.kilograms / si.grams,
                    color='black')

        pyplot.step(setup.radius_bins_edges[:-1] * si.metres / si.micrometres,
                    vals * si.kilograms / si.grams,
                    where='post',
                    label=f"t = {t}s")
        pyplot.grid()
        pyplot.xscale('log')
        pyplot.xlabel('particle radius [µm]')
        pyplot.ylabel('dm/dlnr [g/m^3/(unit dr/r)]')
        pyplot.legend()
示例#2
0
class Settings:
    init_x_min = phys.volume(radius=3.94 * si.micrometre)
    init_x_max = phys.volume(radius=25 * si.micrometres)

    n_sd = 2**13
    n_part = 239 / si.cm**3
    X0 = 4 / 3 * np.pi * (10 * si.micrometres)**3
    dv = 1e1 * si.metres**3  # 1e6 do not work with ThrustRTC (overflow?)
    norm_factor = n_part * dv
    rho = 1000 * si.kilogram / si.metre**3
    dt = 1 * si.seconds
    adaptive = False
    seed = 44
    _steps = [200 * i for i in range(10)]

    @property
    def steps(self):
        return [int(step / self.dt) for step in self._steps]

    kernel = Geometric(collection_efficiency=1)
    spectrum = Exponential(norm_factor=norm_factor, scale=X0)

    # TODO 220 instead of 200 to smoothing
    radius_bins_edges = np.logspace(np.log10(3.94 * si.um),
                                    np.log10(220 * si.um),
                                    num=100,
                                    endpoint=True)
示例#3
0
class SetupA:
    init_x_min = phys.volume(radius=10 *
                             si.micrometres)  # not given in the paper
    init_x_max = phys.volume(radius=100 *
                             si.micrometres)  # not given in the paper

    n_sd = 2**13
    n_part = 2**23 / si.metre**3
    X0 = 4 / 3 * np.pi * (30.531 * si.micrometres)**3
    dv = 1e6 * si.metres**3
    norm_factor = n_part * dv
    rho = 1000 * si.kilogram / si.metre**3
    dt = 1 * si.seconds
    seed = 44
    steps = [0, 1200, 2400, 3600]

    kernel = Golovin(b=1.5e3 / si.second)
    spectrum = Exponential(norm_factor=norm_factor, scale=X0)

    radius_bins_edges = np.logspace(np.log10(10 * si.um),
                                    np.log10(5e3 * si.um),
                                    num=64,
                                    endpoint=True)

    backend = Default
示例#4
0
    def plot_analytic_solution(self, settings, t, spectrum=None):
        if t == 0:
            analytic_solution = settings.spectrum.size_distribution
        else:
            analytic_solution = lambda x: settings.norm_factor * settings.kernel.analytic_solution(
                x=x, t=t, x_0=settings.X0, N_0=settings.n_part)

        volume_bins_edges = phys.volume(settings.radius_bins_edges)
        dm = np.diff(volume_bins_edges)
        dr = np.diff(settings.radius_bins_edges)

        pdf_m_x = volume_bins_edges[:-1] + dm / 2
        pdf_m_y = analytic_solution(pdf_m_x)

        pdf_r_x = settings.radius_bins_edges[:-1] + dr / 2
        pdf_r_y = pdf_m_y * dm / dr * pdf_r_x

        x = pdf_r_x * si.metres / si.micrometres
        y_true = pdf_r_y * phys.volume(
            radius=pdf_r_x
        ) * settings.rho / settings.dv * si.kilograms / si.grams

        self.ax.plot(x, y_true, color='black')

        if spectrum is not None:
            y = spectrum * si.kilograms / si.grams
            error = error_measure(y, y_true, x)
            self.title = f'error: {error:.2f}'  # TODO: rename "error measure" + unit
示例#5
0
def moist_environment_init(
        attributes,
        environment,
        spatial_discretisation,
        spectral_discretisation,
        spectrum_per_mass_of_dry_air,
        r_range,
        kappa,
        enable_temperatures=False
):
    with np.errstate(all='raise'):
        positions = spatial_discretisation(environment.mesh.grid, environment.core.n_sd)
        attributes['cell id'], attributes['cell origin'], attributes['position in cell'] = \
            environment.mesh.cellular_attributes(positions)
        r_dry, n_per_kg = spectral_discretisation(environment.core.n_sd, spectrum_per_mass_of_dry_air, r_range)
        T = environment['T'].to_ndarray()
        p = environment['p'].to_ndarray()
        RH = environment['RH'].to_ndarray()
        r_wet = r_wet_init_impl(r_dry, T, p, RH, attributes['cell id'], kappa)
        rhod = environment['rhod'].to_ndarray()
        n_per_m3 = n_init(n_per_kg, rhod, environment.mesh, attributes['cell id'])

    if enable_temperatures:
        attributes['temperature'] = temperature_init(environment, attributes['cell id'])
    attributes['n'] = n_per_m3
    attributes['volume'] = phys.volume(radius=r_wet)
    attributes['dry volume'] = phys.volume(radius=r_dry)
示例#6
0
    def init_attributes(self,
                        *,
                        spatial_discretisation,
                        spectral_discretisation,
                        kappa,
                        enable_temperatures=False,
                        rtol=default_rtol):
        # TODO move to one method
        super().sync()
        self.notify()

        attributes = {}
        with np.errstate(all='raise'):
            positions = spatial_discretisation.sample(self.mesh.grid,
                                                      self.core.n_sd)
            attributes['cell id'], attributes['cell origin'], attributes['position in cell'] = \
                self.mesh.cellular_attributes(positions)
            r_dry, n_per_kg = spectral_discretisation.sample(self.core.n_sd)
            r_wet = r_wet_init(r_dry, self, attributes['cell id'], kappa, rtol)
            rhod = self['rhod'].to_ndarray()
            cell_id = attributes['cell id']
            domain_volume = np.prod(np.array(self.mesh.size))

        if enable_temperatures:
            attributes['temperature'] = temperature_init(
                self, attributes['cell id'])
        attributes['n'] = discretise_n(n_per_kg * rhod[cell_id] *
                                       domain_volume)
        attributes['volume'] = phys.volume(radius=r_wet)
        attributes['dry volume'] = phys.volume(radius=r_dry)

        return attributes
示例#7
0
 def get(self):
     self.download_moment_to_buffer(
         'volume',
         rank=0,
         filter_range=(phys.volume(self.radius_range[0]),
                       phys.volume(self.radius_range[1])))
     self.buffer[:] /= self.core.mesh.dv
     const.convert_to(self.buffer, const.si.centimetre**-3)
     return self.buffer
示例#8
0
    def init_attributes(self,
                        *,
                        n_in_dv: [float, np.ndarray],
                        kappa: float,
                        r_dry: [float, np.ndarray],
                        rtol=default_rtol):
        if not isinstance(n_in_dv, np.ndarray):
            r_dry = np.array([r_dry])
            n_in_dv = np.array([n_in_dv])

        attributes = {}
        attributes['dry volume'] = phys.volume(radius=r_dry)
        attributes['n'] = discretise_n(n_in_dv)
        r_wet = r_wet_init(r_dry, self, np.zeros_like(attributes['n']), kappa,
                           rtol)
        attributes['volume'] = phys.volume(radius=r_wet)
        return attributes
示例#9
0
    def __init__(self, setup):

        dt_output = setup.total_time / setup.n_steps  # TODO: overwritten in jupyter example
        self.n_substeps = 1  # TODO:
        while (dt_output / self.n_substeps >= setup.dt_max):
            self.n_substeps += 1
        self.bins_edges = phys.volume(setup.r_bins_edges)
        particles_builder = ParticlesBuilder(backend=setup.backend,
                                             n_sd=setup.n_sd)
        particles_builder.set_environment(
            MoistLagrangianParcelAdiabatic, {
                "dt": dt_output / self.n_substeps,
                "mass_of_dry_air": setup.mass_of_dry_air,
                "p0": setup.p0,
                "q0": setup.q0,
                "T0": setup.T0,
                "w": setup.w,
                "z0": setup.z0
            })

        environment = particles_builder.particles.environment
        r_wet = r_wet_init(setup.r_dry, environment, np.zeros_like(setup.n),
                           setup.kappa)
        particles_builder.register_dynamic(
            Condensation, {
                "kappa": setup.kappa,
                "coord": setup.coord,
                "adaptive": setup.adaptive,
                "rtol_x": setup.rtol_x,
                "rtol_thd": setup.rtol_thd,
            })
        attributes = {
            'n': setup.n,
            'dry volume': phys.volume(radius=setup.r_dry),
            'volume': phys.volume(radius=r_wet)
        }
        products = {
            ParticlesSizeSpectrum: {
                'v_bins': phys.volume(setup.r_bins_edges)
            },
            CondensationTimestep: {},
            RipeningRate: {}
        }
        self.particles = particles_builder.get_particles(attributes, products)

        self.n_steps = setup.n_steps
示例#10
0
 def get(self):
     self.download_moment_to_buffer(
         'volume',
         rank=0,
         attr_range=[0, phys.volume(self.radius_threshold)])
     self.buffer[:] /= self.particles.mesh.dv
     const.convert_to(self.buffer, const.si.centimetre**-3)
     return self.buffer
示例#11
0
 def get(self):
     self.download_moment_to_buffer('volume', rank=0,
                                    filter_range=[0, phys.volume(self.radius_threshold)])
     result = self.buffer.copy()  # TODO
     self.download_to_buffer(self.core.environment['rhod'])
     result[:] /= self.core.mesh.dv
     result[:] /= self.buffer
     const.convert_to(result, const.si.milligram**-1)
     return result
 def get(self, radius_bins_edges):
     volume_bins_edges = phys.volume(radius_bins_edges)
     vals = np.empty(len(volume_bins_edges) - 1)
     for i in range(len(vals)):
         self.download_moment_to_buffer(attr='volume', rank=1,
                                        attr_range=(volume_bins_edges[i], volume_bins_edges[i + 1]))
         vals[i] = self.buffer[0]
         self.download_moment_to_buffer(attr='volume', rank=0,
                                        attr_range=(volume_bins_edges[i], volume_bins_edges[i + 1]))
         vals[i] *= self.buffer[0]
     vals *= 1 / np.diff(np.log(radius_bins_edges)) / self.particles.mesh.dv
     return vals
示例#13
0
    def __init__(self, setup):

        dt_output = setup.total_time / setup.n_steps  # TODO: overwritten in jupyter example
        self.n_substeps = 1  # TODO
        while (dt_output / self.n_substeps >= setup.dt_max):
            self.n_substeps += 1
        self.bins_edges = phys.volume(setup.r_bins_edges)
        particles_builder = Builder(backend=setup.backend, n_sd=setup.n_sd)
        particles_builder.set_environment(
            MoistLagrangianParcelAdiabatic(
                dt=dt_output / self.n_substeps,
                mass_of_dry_air=setup.mass_of_dry_air,
                p0=setup.p0,
                q0=setup.q0,
                T0=setup.T0,
                w=setup.w,
                z0=setup.z0))

        environment = particles_builder.core.environment
        r_wet = r_wet_init(setup.r_dry, environment, np.zeros_like(setup.n),
                           setup.kappa)
        condensation = Condensation(kappa=setup.kappa,
                                    coord=setup.coord,
                                    adaptive=setup.adaptive,
                                    rtol_x=setup.rtol_x,
                                    rtol_thd=setup.rtol_thd)
        particles_builder.add_dynamic(condensation)
        attributes = {
            'n': setup.n,
            'dry volume': phys.volume(radius=setup.r_dry),
            'volume': phys.volume(radius=r_wet)
        }
        products = [
            ParticlesWetSizeSpectrum(v_bins=phys.volume(setup.r_bins_edges)),
            CondensationTimestep(),
            RipeningRate()
        ]
        self.particles = particles_builder.build(attributes, products)

        self.n_steps = setup.n_steps
示例#14
0
    def __init__(self, settings, backend=CPU):

        dt_output = settings.total_time / settings.n_steps  # TODO: overwritten in jupyter example
        self.n_substeps = 1  # TODO
        while (dt_output / self.n_substeps >= settings.dt_max):
            self.n_substeps += 1
        self.bins_edges = phys.volume(settings.r_bins_edges)
        builder = Builder(backend=backend, n_sd=settings.n_sd)
        builder.set_environment(
            Parcel(dt=dt_output / self.n_substeps,
                   mass_of_dry_air=settings.mass_of_dry_air,
                   p0=settings.p0,
                   q0=settings.q0,
                   T0=settings.T0,
                   w=settings.w,
                   z0=settings.z0))

        environment = builder.core.environment
        builder.add_dynamic(AmbientThermodynamics())
        condensation = Condensation(kappa=settings.kappa,
                                    coord=settings.coord,
                                    adaptive=settings.adaptive,
                                    rtol_x=settings.rtol_x,
                                    rtol_thd=settings.rtol_thd)
        builder.add_dynamic(condensation)

        products = [
            ParticlesWetSizeSpectrum(
                v_bins=phys.volume(settings.r_bins_edges)),
            CondensationTimestep(),
            RipeningRate()
        ]

        attributes = environment.init_attributes(n_in_dv=settings.n,
                                                 kappa=settings.kappa,
                                                 r_dry=settings.r_dry)

        self.core = builder.build(attributes, products)

        self.n_steps = settings.n_steps
示例#15
0
    def __init__(self, setup):
        t_half = setup.z_half / setup.w_avg

        dt_output = (2 * t_half) / setup.n_output
        self.n_substeps = 1
        while dt_output / self.n_substeps >= setup.dt_max:  # TODO dt_max
            self.n_substeps += 1

        particles_builder = ParticlesBuilder(backend=setup.backend, n_sd=1)
        particles_builder.set_environment(
            MoistLagrangianParcelAdiabatic, {
                "dt": dt_output / self.n_substeps,
                "mass_of_dry_air": setup.mass_of_dry_air,
                "p0": setup.p0,
                "q0": setup.q0,
                "T0": setup.T0,
                "w": setup.w
            })

        particles_builder.register_dynamic(
            Condensation, {
                "kappa": setup.kappa,
                "rtol_x": setup.rtol_x,
                "rtol_thd": setup.rtol_thd,
            })
        attributes = {}
        r_dry = np.array([setup.r_dry])
        attributes['dry volume'] = phys.volume(radius=r_dry)
        attributes['n'] = np.array([setup.n_in_dv], dtype=np.int64)
        environment = particles_builder.particles.environment
        r_wet = r_wet_init(r_dry, environment, np.zeros_like(attributes['n']),
                           setup.kappa)
        attributes['volume'] = phys.volume(radius=r_wet)
        products = {ParticleMeanRadius: {}}

        self.particles = particles_builder.get_particles(attributes, products)

        self.n_output = setup.n_output
示例#16
0
class Setup:
    init_x_min = phys.volume(radius=3.94 * si.micrometre)
    init_x_max = phys.volume(radius=25 * si.micrometres)

    n_sd = 2**13
    n_part = 239 / si.cm**3
    X0 = 4 / 3 * np.pi * (10 * si.micrometres)**3
    dv = 1e1 * si.metres**3
    norm_factor = n_part * dv
    rho = 1000 * si.kilogram / si.metre**3
    dt = 10 * si.seconds
    seed = 44
    steps = [200 * i for i in range(10)]

    kernel = Gravitational(collection_efficiency=None)
    spectrum = Exponential(norm_factor=norm_factor, scale=X0)

    radius_bins_edges = np.logspace(np.log10(3.94 * si.um),
                                    np.log10(200 * si.um),
                                    num=128,
                                    endpoint=True)

    backend = Default
示例#17
0
    def __init__(self, setup):
        t_half = setup.z_half / setup.w_avg

        dt_output = (2 * t_half) / setup.n_output
        self.n_substeps = 1
        while dt_output / self.n_substeps >= setup.dt_max:  # TODO dt_max
            self.n_substeps += 1

        builder = Builder(backend=setup.backend, n_sd=1)
        builder.set_environment(
            MoistLagrangianParcelAdiabatic(
                dt=dt_output / self.n_substeps,
                mass_of_dry_air=setup.mass_of_dry_air,
                p0=setup.p0,
                q0=setup.q0,
                T0=setup.T0,
                w=setup.w))

        builder.add_dynamic(
            Condensation(
                kappa=setup.kappa,
                rtol_x=setup.rtol_x,
                rtol_thd=setup.rtol_thd,
            ))
        attributes = {}
        r_dry = np.array([setup.r_dry])
        attributes['dry volume'] = phys.volume(radius=r_dry)
        attributes['n'] = np.array([setup.n_in_dv], dtype=np.int64)
        environment = builder.core.environment
        r_wet = r_wet_init(r_dry, environment, np.zeros_like(attributes['n']),
                           setup.kappa)
        attributes['volume'] = phys.volume(radius=r_wet)
        products = [ParticleMeanRadius(), CondensationTimestep()]

        self.core = builder.build(attributes, products)

        self.n_output = setup.n_output
示例#18
0
class Setup:
    backend = Default
    condensation_coord = 'volume'

    condensation_rtol_x = condensation.default_rtol_x
    condensation_rtol_thd = condensation.default_rtol_thd
    adaptive = True

    grid = (25, 25)
    size = (1500 * si.metres, 1500 * si.metres)
    n_sd_per_gridbox = 20
    rho_w_max = .6 * si.metres / si.seconds * (si.kilogram / si.metre**3)

    # output steps
    n_steps = 3600
    outfreq = 60
    dt = 1 * si.seconds

    v_bins = phys.volume(
        np.logspace(np.log10(0.01 * si.micrometre),
                    np.log10(100 * si.micrometre),
                    101,
                    endpoint=True))

    @property
    def steps(self):
        return np.arange(0, self.n_steps + 1, self.outfreq)

    # TODO: second mode
    spectrum_per_mass_of_dry_air = Lognormal(norm_factor=40 /
                                             si.centimetre**3 / const.rho_STP,
                                             m_mode=0.15 * si.micrometre,
                                             s_geom=1.6)

    processes = {
        "particle advection": True,
        "fluid advection": True,
        "coalescence": False,
        "condensation": True,
        "sedimentation": False,
        # "relaxation": False  # TODO
    }

    enable_particle_temperatures = False

    mpdata_iters = 2
    mpdata_iga = True
    mpdata_fct = True
    mpdata_tot = True

    th_std0 = 289 * si.kelvins
    qv0 = 7.5 * si.grams / si.kilogram
    p0 = 1015 * si.hectopascals
    kappa = 1

    @property
    def field_values(self):
        return {'th': phys.th_dry(self.th_std0, self.qv0), 'qv': self.qv0}

    @property
    def n_sd(self):
        return self.grid[0] * self.grid[1] * self.n_sd_per_gridbox

    def stream_function(self, xX, zZ):
        X = self.size[0]
        return -self.rho_w_max * X / np.pi * np.sin(np.pi * zZ) * np.cos(
            2 * np.pi * xX)

    def rhod(self, zZ):
        Z = self.size[1]
        z = zZ * Z  # :)

        # hydrostatic profile
        kappa = const.Rd / const.c_pd
        arg = np.power(
            self.p0 / const.p1000,
            kappa) - z * kappa * const.g / self.th_std0 / phys.R(self.qv0)
        p = const.p1000 * np.power(arg, 1 / kappa)

        # np.testing.assert_array_less(p, Setup.p0)  # TODO: less or equal

        # density using "dry" potential temp.
        pd = p * (1 - self.qv0 / (self.qv0 + const.eps))
        rhod = pd / (np.power(p / const.p1000, kappa) * const.Rd *
                     self.th_std0)

        return rhod

    # initial dry radius discretisation range
    r_min = .01 * si.micrometre
    r_max = 5 * si.micrometre

    kernel = Gravitational(collection_efficiency=1)  # [s-1]  # TODO!
    aerosol_radius_threshold = 1 * si.micrometre

    n_spin_up = 1 * si.hour / dt
示例#19
0
def test_initialisation(plot=False):
    # TODO: seed as a part of setup?
    setup = Setup()
    setup.n_steps = -1
    setup.grid = (10, 5)
    setup.n_sd_per_gridbox = 2000

    simulation = Simulation(setup, None)

    n_bins = 32
    n_levels = setup.grid[1]
    n_cell = np.prod(np.array(setup.grid))
    n_moments = 1

    v_bins = np.logspace((np.log10(phys.volume(radius=setup.r_min))),
                         (np.log10(phys.volume(radius=10 * setup.r_max))),
                         num=n_bins,
                         endpoint=True)
    r_bins = phys.radius(volume=v_bins)

    histogram_dry = np.empty((len(r_bins) - 1, n_levels))
    histogram_wet = np.empty_like(histogram_dry)

    moment_0 = setup.backend.array(n_cell, dtype=int)
    moments = setup.backend.array((n_moments, n_cell), dtype=float)
    tmp = np.empty(n_cell)
    simulation.reinit()

    # Act (moments)
    simulation.run()
    particles = simulation.particles
    environment = simulation.particles.environment
    rhod = setup.backend.to_ndarray(environment["rhod"]).reshape(
        setup.grid).mean(axis=0)

    for i in range(len(histogram_dry)):
        particles.state.moments(moment_0,
                                moments,
                                specs={},
                                attr_name='dry volume',
                                attr_range=(v_bins[i], v_bins[i + 1]))
        particles.backend.download(moment_0, tmp)
        histogram_dry[i, :] = tmp.reshape(
            setup.grid).sum(axis=0) / (particles.mesh.dv * setup.grid[0])

        particles.state.moments(moment_0,
                                moments,
                                specs={},
                                attr_name='volume',
                                attr_range=(v_bins[i], v_bins[i + 1]))
        particles.backend.download(moment_0, tmp)
        histogram_wet[i, :] = tmp.reshape(
            setup.grid).sum(axis=0) / (particles.mesh.dv * setup.grid[0])

    # Plot
    if plot:
        for level in range(0, n_levels):
            color = str(.5 * (2 + (level / (n_levels - 1))))
            pyplot.step(r_bins[:-1] * si.metres / si.micrometres,
                        histogram_dry[:, level] / si.metre**3 *
                        si.centimetre**3,
                        where='post',
                        color=color,
                        label="level " + str(level))
            pyplot.step(r_bins[:-1] * si.metres / si.micrometres,
                        histogram_wet[:, level] / si.metre**3 *
                        si.centimetre**3,
                        where='post',
                        color=color,
                        linestyle='--')
        pyplot.grid()
        pyplot.xscale('log')
        pyplot.xlabel('particle radius [µm]')
        pyplot.ylabel('concentration per bin [cm^{-3}]')
        pyplot.legend()
        pyplot.show()

    # Assert - location of maximum
    for level in range(n_levels):
        real_max = setup.spectrum_per_mass_of_dry_air.distribution_params[2]
        idx_max_dry = np.argmax(histogram_dry[:, level])
        idx_max_wet = np.argmax(histogram_wet[:, level])
        assert r_bins[idx_max_dry] < real_max < r_bins[idx_max_dry + 1]
        assert idx_max_dry < idx_max_wet

    # Assert - total number
    for level in reversed(range(n_levels)):
        mass_conc_dry = np.sum(histogram_dry[:, level]) / rhod[level]
        mass_conc_wet = np.sum(histogram_wet[:, level]) / rhod[level]
        mass_conc_STP = setup.spectrum_per_mass_of_dry_air.norm_factor
        assert .5 * mass_conc_STP < mass_conc_dry < 1.5 * mass_conc_STP
        np.testing.assert_approx_equal(mass_conc_dry, mass_conc_wet)

    # Assert - decreasing number density
    total_above = 0
    for level in reversed(range(n_levels)):
        total_below = np.sum(histogram_dry[:, level])
        assert total_below > total_above
        total_above = total_below
示例#20
0
class Settings:
    def __dir__(self) -> Iterable[str]:
        return 'dt', 'grid', 'size', 'n_spin_up', 'versions', 'outfreq'

    def __init__(self):
        key_packages = (PySDM, numba, numpy, scipy)
        self.versions = str(
            {pkg.__name__: pkg.__version__
             for pkg in key_packages})

    # TODO: move all below into __init__ as self.* variables

    condensation_coord = 'volume logarithm'

    condensation_rtol_x = condensation.default_rtol_x
    condensation_rtol_thd = condensation.default_rtol_thd
    adaptive = True

    grid = (25, 25)
    size = (1500 * si.metres, 1500 * si.metres)
    n_sd_per_gridbox = 20
    rho_w_max = .6 * si.metres / si.seconds * (si.kilogram / si.metre**3)

    # output steps
    n_steps = 5400
    outfreq = 60
    dt = 1 * si.seconds

    n_spin_up = 1 * si.hour / dt

    v_bins = phys.volume(
        np.logspace(np.log10(0.01 * si.micrometre),
                    np.log10(100 * si.micrometre),
                    101,
                    endpoint=True))

    @property
    def steps(self):
        return np.arange(0, self.n_steps + 1, self.outfreq)

    mode_1 = Lognormal(norm_factor=60 / si.centimetre**3 / const.rho_STP,
                       m_mode=0.04 * si.micrometre,
                       s_geom=1.4)
    mode_2 = Lognormal(norm_factor=40 / si.centimetre**3 / const.rho_STP,
                       m_mode=0.15 * si.micrometre,
                       s_geom=1.6)
    spectrum_per_mass_of_dry_air = Sum((mode_1, mode_2))

    processes = {
        "particle advection": True,
        "fluid advection": True,
        "coalescence": True,
        "condensation": True,
        "sedimentation": True,
        # "relaxation": False  # TODO
    }

    enable_particle_temperatures = False

    mpdata_iters = 2
    mpdata_iga = True
    mpdata_fct = True
    mpdata_tot = True

    th_std0 = 289 * si.kelvins
    qv0 = 7.5 * si.grams / si.kilogram
    p0 = 1015 * si.hectopascals
    kappa = 1

    @property
    def field_values(self):
        return {'th': phys.th_dry(self.th_std0, self.qv0), 'qv': self.qv0}

    @property
    def n_sd(self):
        return self.grid[0] * self.grid[1] * self.n_sd_per_gridbox

    def stream_function(self, xX, zZ):
        X = self.size[0]
        return -self.rho_w_max * X / np.pi * np.sin(np.pi * zZ) * np.cos(
            2 * np.pi * xX)

    def rhod(self, zZ):
        Z = self.size[1]
        z = zZ * Z  # :(!

        # TODO: move to PySDM/physics
        # hydrostatic profile
        kappa = const.Rd / const.c_pd
        arg = np.power(
            self.p0 / const.p1000,
            kappa) - z * kappa * const.g / self.th_std0 / phys.R(self.qv0)
        p = const.p1000 * np.power(arg, 1 / kappa)

        # density using "dry" potential temp.
        pd = p * (1 - self.qv0 / (self.qv0 + const.eps))
        rhod = pd / (np.power(p / const.p1000, kappa) * const.Rd *
                     self.th_std0)

        return rhod

    kernel = Geometric(collection_efficiency=1)
    aerosol_radius_threshold = .5 * si.micrometre
    drizzle_radius_threshold = 25 * si.micrometre