Module PySDM.dynamics.condensation

bespoke condensational growth solver with implicit-in-particle-size integration and adaptive timestepping

Expand source code
"""
bespoke condensational growth solver
with implicit-in-particle-size integration and adaptive timestepping
"""

from collections import namedtuple

import numpy as np

from ..physics import si

DEFAULTS = namedtuple("_", ("rtol_x", "rtol_thd", "cond_range", "schedule"))(
    rtol_x=1e-6,
    rtol_thd=1e-6,
    cond_range=(1e-4 * si.second, 1 * si.second),
    schedule="dynamic",
)


class Condensation:  # pylint: disable=too-many-instance-attributes
    def __init__(
        self,
        *,
        rtol_x=DEFAULTS.rtol_x,
        rtol_thd=DEFAULTS.rtol_thd,
        substeps: int = 1,
        adaptive: bool = True,
        dt_cond_range: tuple = DEFAULTS.cond_range,
        schedule: str = DEFAULTS.schedule,
        max_iters: int = 16,
        update_thd: bool = True,
    ):
        if adaptive and substeps != 1:
            raise ValueError(
                "if specifying substeps count manually, adaptivity must be disabled"
            )

        self.particulator = None
        self.enable = True

        self.rtol_x = rtol_x
        self.rtol_thd = rtol_thd

        self.rh_max = None
        self.success = None

        self.__substeps = substeps
        self.adaptive = adaptive
        self.counters = {}
        self.dt_cond_range = dt_cond_range
        self.schedule = schedule
        self.max_iters = max_iters

        self.cell_order = None

        self.update_thd = update_thd

    def register(self, builder):
        self.particulator = builder.particulator

        builder._set_condensation_parameters(
            dt_range=self.dt_cond_range,
            adaptive=self.adaptive,
            fuse=32,
            multiplier=2,
            RH_rtol=1e-7,
            max_iters=self.max_iters,
        )
        builder.request_attribute("critical volume")
        builder.request_attribute("kappa")
        builder.request_attribute("dry volume organic fraction")
        builder.request_attribute("Reynolds number")

        for counter in ("n_substeps", "n_activating", "n_deactivating", "n_ripening"):
            self.counters[counter] = self.particulator.Storage.empty(
                self.particulator.mesh.n_cell, dtype=int
            )
            if counter == "n_substeps":
                self.counters[counter][:] = self.__substeps if not self.adaptive else -1
            else:
                self.counters[counter][:] = -1

        self.rh_max = self.particulator.Storage.empty(
            self.particulator.mesh.n_cell, dtype=float
        )
        self.rh_max[:] = np.nan
        self.success = self.particulator.Storage.empty(
            self.particulator.mesh.n_cell, dtype=bool
        )
        self.success[:] = False
        self.cell_order = np.arange(self.particulator.mesh.n_cell)

    def __call__(self):
        if self.enable:
            if self.schedule == "dynamic":
                self.cell_order = np.argsort(self.counters["n_substeps"])
            elif self.schedule == "static":
                pass
            else:
                raise NotImplementedError()

            self.particulator.condensation(
                rtol_x=self.rtol_x,
                rtol_thd=self.rtol_thd,
                counters=self.counters,
                RH_max=self.rh_max,
                success=self.success,
                cell_order=self.cell_order,
            )
            if not self.success.all():
                raise RuntimeError("Condensation failed")
            if not self.update_thd:
                self.particulator.environment.get_predicted("thd").ravel(
                    self.particulator.environment.get_thd()
                )
            # note: this makes order of dynamics matter
            #       (e.g., condensation after chemistry or before)
            self.particulator.update_TpRH()

            if self.adaptive:
                self.counters["n_substeps"][:] = np.maximum(
                    self.counters["n_substeps"][:],
                    int(self.particulator.dt / self.dt_cond_range[1]),
                )
                if self.dt_cond_range[0] != 0:
                    self.counters["n_substeps"][:] = np.minimum(
                        self.counters["n_substeps"][:],
                        int(self.particulator.dt / self.dt_cond_range[0]),
                    )

Classes

class Condensation (*, rtol_x=1e-06, rtol_thd=1e-06, substeps: int = 1, adaptive: bool = True, dt_cond_range: tuple = (0.0001, 1.0), schedule: str = 'dynamic', max_iters: int = 16, update_thd: bool = True)
Expand source code
class Condensation:  # pylint: disable=too-many-instance-attributes
    def __init__(
        self,
        *,
        rtol_x=DEFAULTS.rtol_x,
        rtol_thd=DEFAULTS.rtol_thd,
        substeps: int = 1,
        adaptive: bool = True,
        dt_cond_range: tuple = DEFAULTS.cond_range,
        schedule: str = DEFAULTS.schedule,
        max_iters: int = 16,
        update_thd: bool = True,
    ):
        if adaptive and substeps != 1:
            raise ValueError(
                "if specifying substeps count manually, adaptivity must be disabled"
            )

        self.particulator = None
        self.enable = True

        self.rtol_x = rtol_x
        self.rtol_thd = rtol_thd

        self.rh_max = None
        self.success = None

        self.__substeps = substeps
        self.adaptive = adaptive
        self.counters = {}
        self.dt_cond_range = dt_cond_range
        self.schedule = schedule
        self.max_iters = max_iters

        self.cell_order = None

        self.update_thd = update_thd

    def register(self, builder):
        self.particulator = builder.particulator

        builder._set_condensation_parameters(
            dt_range=self.dt_cond_range,
            adaptive=self.adaptive,
            fuse=32,
            multiplier=2,
            RH_rtol=1e-7,
            max_iters=self.max_iters,
        )
        builder.request_attribute("critical volume")
        builder.request_attribute("kappa")
        builder.request_attribute("dry volume organic fraction")
        builder.request_attribute("Reynolds number")

        for counter in ("n_substeps", "n_activating", "n_deactivating", "n_ripening"):
            self.counters[counter] = self.particulator.Storage.empty(
                self.particulator.mesh.n_cell, dtype=int
            )
            if counter == "n_substeps":
                self.counters[counter][:] = self.__substeps if not self.adaptive else -1
            else:
                self.counters[counter][:] = -1

        self.rh_max = self.particulator.Storage.empty(
            self.particulator.mesh.n_cell, dtype=float
        )
        self.rh_max[:] = np.nan
        self.success = self.particulator.Storage.empty(
            self.particulator.mesh.n_cell, dtype=bool
        )
        self.success[:] = False
        self.cell_order = np.arange(self.particulator.mesh.n_cell)

    def __call__(self):
        if self.enable:
            if self.schedule == "dynamic":
                self.cell_order = np.argsort(self.counters["n_substeps"])
            elif self.schedule == "static":
                pass
            else:
                raise NotImplementedError()

            self.particulator.condensation(
                rtol_x=self.rtol_x,
                rtol_thd=self.rtol_thd,
                counters=self.counters,
                RH_max=self.rh_max,
                success=self.success,
                cell_order=self.cell_order,
            )
            if not self.success.all():
                raise RuntimeError("Condensation failed")
            if not self.update_thd:
                self.particulator.environment.get_predicted("thd").ravel(
                    self.particulator.environment.get_thd()
                )
            # note: this makes order of dynamics matter
            #       (e.g., condensation after chemistry or before)
            self.particulator.update_TpRH()

            if self.adaptive:
                self.counters["n_substeps"][:] = np.maximum(
                    self.counters["n_substeps"][:],
                    int(self.particulator.dt / self.dt_cond_range[1]),
                )
                if self.dt_cond_range[0] != 0:
                    self.counters["n_substeps"][:] = np.minimum(
                        self.counters["n_substeps"][:],
                        int(self.particulator.dt / self.dt_cond_range[0]),
                    )

Methods

def register(self, builder)
Expand source code
def register(self, builder):
    self.particulator = builder.particulator

    builder._set_condensation_parameters(
        dt_range=self.dt_cond_range,
        adaptive=self.adaptive,
        fuse=32,
        multiplier=2,
        RH_rtol=1e-7,
        max_iters=self.max_iters,
    )
    builder.request_attribute("critical volume")
    builder.request_attribute("kappa")
    builder.request_attribute("dry volume organic fraction")
    builder.request_attribute("Reynolds number")

    for counter in ("n_substeps", "n_activating", "n_deactivating", "n_ripening"):
        self.counters[counter] = self.particulator.Storage.empty(
            self.particulator.mesh.n_cell, dtype=int
        )
        if counter == "n_substeps":
            self.counters[counter][:] = self.__substeps if not self.adaptive else -1
        else:
            self.counters[counter][:] = -1

    self.rh_max = self.particulator.Storage.empty(
        self.particulator.mesh.n_cell, dtype=float
    )
    self.rh_max[:] = np.nan
    self.success = self.particulator.Storage.empty(
        self.particulator.mesh.n_cell, dtype=bool
    )
    self.success[:] = False
    self.cell_order = np.arange(self.particulator.mesh.n_cell)