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()
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)
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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