dapper.da_methods

Contains the data assimilation methods included with DAPPER.

Also see the section on DA Methods in the main README for an overview of the methods included with DAPPER.

Defining your own method

Follow the example of one of the methods within one of the sub-directories/packages. The simplest example is perhaps dapper.da_methods.ensemble.EnKF.

General advice for programming/debugging scientific experiments

  • Start with something simple. This helps make sure the basics of the experiment are reasonable. For example, start with

    • a pre-existing example,
    • something you are able to reproduce,
    • a small/simple model.

      • Set the observation error to be small.
      • Observe everything.
      • Don't include model error and/or noise to begin with.
  • Additionally, test a simple/baseline method to begin with. When including an ensemble method, start with using a large ensemble, and introduce localisation later.

  • Take incremental steps towards your ultimate experiment setup. Validate each incremental setup with prints/plots. If results change, make sure you understand why.

  • Use short experiment duration. You probably don't need statistical significance while debugging.

  1"""Contains the data assimilation methods included with DAPPER.
  2
  3.. include:: ./README.md
  4"""
  5from pathlib import Path
  6
  7
  8def da_method(*default_dataclasses):
  9    """Turn a dataclass-style class into a DA method for DAPPER (`xp`).
 10
 11    This decorator applies to classes that define DA methods.
 12    An instances of the resulting class is referred to (in DAPPER)
 13    as an `xp` (short for experiment).
 14
 15    The decorated classes are defined like a `dataclass`,
 16    but are decorated by `@da_method()` instead of `@dataclass`.
 17
 18    .. note::
 19        The classes must define a method called `assimilate`.
 20        This method gets slightly enhanced by this wrapper which provides:
 21
 22        - Initialisation of the `Stats` object, accessible by `self.stats`.
 23        - `fail_gently` functionality.
 24        - Duration timing
 25        - Progressbar naming magic.
 26
 27    Example:
 28    >>> @da_method()
 29    ... class Sleeper():
 30    ...     "Do nothing."
 31    ...     seconds : int  = 10
 32    ...     success : bool = True
 33    ...     def assimilate(self, *args, **kwargs):
 34    ...         for k in range(self.seconds):
 35    ...             time.sleep(1)
 36    ...         if not self.success:
 37    ...             raise RuntimeError("Sleep over. Failing as intended.")
 38
 39    Internally, `da_method` is just like `dataclass`,
 40    except that adds an outer layer
 41    (hence the empty parantheses in the above)
 42    which enables defining default parameters which can be inherited,
 43    similar to subclassing.
 44
 45    Example:
 46    >>> class ens_defaults:
 47    ...     infl : float = 1.0
 48    ...     rot  : bool  = False
 49
 50    >>> @da_method(ens_defaults)
 51    ... class EnKF:
 52    ...     N     : int
 53    ...     upd_a : str = "Sqrt"
 54    ...
 55    ...     def assimilate(self, HMM, xx, yy):
 56    ...         ...
 57
 58    .. note::
 59        Apart from what's listed in the above `Note`, there is nothing special to the
 60        resulting `xp`.  That is, just like any Python object, it can serve as a data
 61        container, and you can write any number of attributes to it (at creation-time,
 62        or later).  For example, you can set attributes that are not used by the
 63        `assimilate` method, but are instead used to customize other aspects of the
 64        experiments (see `dapper.xp_launch.run_experiment`).
 65    """
 66    import dataclasses
 67    import functools
 68    import time
 69    from dataclasses import dataclass
 70
 71    import dapper.stats
 72
 73    def dataclass_with_defaults(cls):
 74        """Like `dataclass`, but add some DAPPER-specific things.
 75
 76        This adds `__init__`, `__repr__`, `__eq__`, ...,
 77        but also includes inherited defaults,
 78        ref https://stackoverflow.com/a/58130805,
 79        and enhances the `assimilate` method.
 80        """
 81
 82        def set_field(name, type_, val):
 83            """Set the inherited (i.e. default, i.e. has value) field."""
 84            # Ensure annotations
 85            cls.__annotations__ = getattr(cls, '__annotations__', {})
 86            # Set annotation
 87            cls.__annotations__[name] = type_
 88            # Set value
 89            setattr(cls, name, val)
 90
 91        # APPend default fields without overwriting.
 92        # NB: Don't implement (by PREpending?) non-default args -- to messy!
 93        for default_params in default_dataclasses:
 94            # NB: Calling dataclass twice always makes repr=True
 95            for field in dataclasses.fields(dataclass(default_params)):
 96                if field.name not in cls.__annotations__:
 97                    set_field(field.name, field.type, field)
 98
 99        # Create new class (NB: old/new classes have same id)
100        cls = dataclass(cls)
101
102        # Define the new assimilate method (has bells and whistles)
103        def assimilate(self, HMM, xx, yy, desc=None, fail_gently=False, **stat_kwargs):
104            # Progressbar name
105            pb_name_hook = self.da_method if desc is None else desc # noqa
106
107            # Init stats
108            self.stats = dapper.stats.Stats(self, HMM, xx, yy, **stat_kwargs)
109
110            # Assimilate
111            time0 = time.time()
112            try:
113                _assimilate(self, HMM, xx, yy)
114            except Exception as ERR:
115                if fail_gently:
116                    self.crashed = True
117                    if fail_gently not in ["silent", "quiet"]:
118                        _print_cropped_traceback(ERR)
119                else:
120                    # Don't use _print_cropped_traceback here -- It would
121                    # crop out errors in the DAPPER infrastructure itself.
122                    raise
123            self.stat("duration", time.time()-time0)
124
125        # Overwrite the assimilate method with the new one
126        try:
127            _assimilate = cls.assimilate
128        except AttributeError as error:
129            raise AttributeError(
130                "Classes decorated by da_method()"
131                " must define a method called 'assimilate'.") from error
132        cls.assimilate = functools.wraps(_assimilate)(assimilate)
133
134        # Shortcut for register_stat
135        def stat(self, name, value):
136            dapper.stats.register_stat(self.stats, name, value)
137        cls.stat = stat
138
139        # Make self.__class__.__name__ an attrib.
140        # Used by xpList.table_prep().
141        cls.da_method = cls.__name__
142
143        return cls
144    return dataclass_with_defaults
145
146
147def _print_cropped_traceback(ERR):
148    import inspect
149    import sys
150    import traceback
151
152    # A more "standard" (robust) way:
153    # https://stackoverflow.com/a/32999522
154    def crop_traceback(ERR):
155        msg = "Traceback (most recent call last):\n"
156        try:
157            # If in IPython, use its coloring functionality
158            __IPYTHON__  # type: ignore
159        except (NameError, ImportError):
160            msg += "".join(traceback.format_tb(ERR.__traceback__))
161        else:
162            from IPython.core.debugger import Pdb
163            pdb_instance = Pdb()
164            pdb_instance.curframe = inspect.currentframe()
165
166            import dapper.da_methods
167            keep = False
168            for frame in traceback.walk_tb(ERR.__traceback__):
169                if keep:
170                    msg += pdb_instance.format_stack_entry(frame, context=3)
171                elif frame[0].f_code.co_filename == dapper.da_methods.__file__:
172                    keep = True
173                    msg += "   ⋮ [cropped] \n"
174
175        return msg
176
177    msg = crop_traceback(ERR) + "\nError message: " + str(ERR)
178    msg += ("\n\nResuming execution."
179            "\nIf instead you wish to raise the exceptions as usual,"
180            "\nwhich will halt the execution (and enable post-mortem debug),"
181            "\nthen use `fail_gently=False`")
182    print(msg, file=sys.stderr)
183
184
185# Import all da_methods
186# for _mod in Path(__file__).parent.glob("*.py"):
187#     if _mod != Path(__file__) and not _mod.stem.startswith("_"):
188#         _mod = __import__(__package__ + "." + _mod.stem, fromlist=['*'])
189#         del globals()[_mod.__name__.split(".")[-1]]  # rm module itself
190#         globals().update({k: v for k, v in vars(_mod).items()
191#                           if isinstance(v, type) and hasattr(v, "da_method")})
192
193# The above does not allow for go-to-definition, so
194from .baseline import Climatology, OptInterp, Persistence, PreProg, Var3D
195from .ensemble import LETKF, SL_EAKF, EnKF, EnKF_N, EnKS, EnRTS
196from .extended import ExtKF, ExtRTS
197from .other import LNETF, RHF
198from .particle import OptPF, PartFilt, PFa, PFxN, PFxN_EnKF
199from .variational import Var4D, iEnKS
def da_method(*default_dataclasses):
  9def da_method(*default_dataclasses):
 10    """Turn a dataclass-style class into a DA method for DAPPER (`xp`).
 11
 12    This decorator applies to classes that define DA methods.
 13    An instances of the resulting class is referred to (in DAPPER)
 14    as an `xp` (short for experiment).
 15
 16    The decorated classes are defined like a `dataclass`,
 17    but are decorated by `@da_method()` instead of `@dataclass`.
 18
 19    .. note::
 20        The classes must define a method called `assimilate`.
 21        This method gets slightly enhanced by this wrapper which provides:
 22
 23        - Initialisation of the `Stats` object, accessible by `self.stats`.
 24        - `fail_gently` functionality.
 25        - Duration timing
 26        - Progressbar naming magic.
 27
 28    Example:
 29    >>> @da_method()
 30    ... class Sleeper():
 31    ...     "Do nothing."
 32    ...     seconds : int  = 10
 33    ...     success : bool = True
 34    ...     def assimilate(self, *args, **kwargs):
 35    ...         for k in range(self.seconds):
 36    ...             time.sleep(1)
 37    ...         if not self.success:
 38    ...             raise RuntimeError("Sleep over. Failing as intended.")
 39
 40    Internally, `da_method` is just like `dataclass`,
 41    except that adds an outer layer
 42    (hence the empty parantheses in the above)
 43    which enables defining default parameters which can be inherited,
 44    similar to subclassing.
 45
 46    Example:
 47    >>> class ens_defaults:
 48    ...     infl : float = 1.0
 49    ...     rot  : bool  = False
 50
 51    >>> @da_method(ens_defaults)
 52    ... class EnKF:
 53    ...     N     : int
 54    ...     upd_a : str = "Sqrt"
 55    ...
 56    ...     def assimilate(self, HMM, xx, yy):
 57    ...         ...
 58
 59    .. note::
 60        Apart from what's listed in the above `Note`, there is nothing special to the
 61        resulting `xp`.  That is, just like any Python object, it can serve as a data
 62        container, and you can write any number of attributes to it (at creation-time,
 63        or later).  For example, you can set attributes that are not used by the
 64        `assimilate` method, but are instead used to customize other aspects of the
 65        experiments (see `dapper.xp_launch.run_experiment`).
 66    """
 67    import dataclasses
 68    import functools
 69    import time
 70    from dataclasses import dataclass
 71
 72    import dapper.stats
 73
 74    def dataclass_with_defaults(cls):
 75        """Like `dataclass`, but add some DAPPER-specific things.
 76
 77        This adds `__init__`, `__repr__`, `__eq__`, ...,
 78        but also includes inherited defaults,
 79        ref https://stackoverflow.com/a/58130805,
 80        and enhances the `assimilate` method.
 81        """
 82
 83        def set_field(name, type_, val):
 84            """Set the inherited (i.e. default, i.e. has value) field."""
 85            # Ensure annotations
 86            cls.__annotations__ = getattr(cls, '__annotations__', {})
 87            # Set annotation
 88            cls.__annotations__[name] = type_
 89            # Set value
 90            setattr(cls, name, val)
 91
 92        # APPend default fields without overwriting.
 93        # NB: Don't implement (by PREpending?) non-default args -- to messy!
 94        for default_params in default_dataclasses:
 95            # NB: Calling dataclass twice always makes repr=True
 96            for field in dataclasses.fields(dataclass(default_params)):
 97                if field.name not in cls.__annotations__:
 98                    set_field(field.name, field.type, field)
 99
100        # Create new class (NB: old/new classes have same id)
101        cls = dataclass(cls)
102
103        # Define the new assimilate method (has bells and whistles)
104        def assimilate(self, HMM, xx, yy, desc=None, fail_gently=False, **stat_kwargs):
105            # Progressbar name
106            pb_name_hook = self.da_method if desc is None else desc # noqa
107
108            # Init stats
109            self.stats = dapper.stats.Stats(self, HMM, xx, yy, **stat_kwargs)
110
111            # Assimilate
112            time0 = time.time()
113            try:
114                _assimilate(self, HMM, xx, yy)
115            except Exception as ERR:
116                if fail_gently:
117                    self.crashed = True
118                    if fail_gently not in ["silent", "quiet"]:
119                        _print_cropped_traceback(ERR)
120                else:
121                    # Don't use _print_cropped_traceback here -- It would
122                    # crop out errors in the DAPPER infrastructure itself.
123                    raise
124            self.stat("duration", time.time()-time0)
125
126        # Overwrite the assimilate method with the new one
127        try:
128            _assimilate = cls.assimilate
129        except AttributeError as error:
130            raise AttributeError(
131                "Classes decorated by da_method()"
132                " must define a method called 'assimilate'.") from error
133        cls.assimilate = functools.wraps(_assimilate)(assimilate)
134
135        # Shortcut for register_stat
136        def stat(self, name, value):
137            dapper.stats.register_stat(self.stats, name, value)
138        cls.stat = stat
139
140        # Make self.__class__.__name__ an attrib.
141        # Used by xpList.table_prep().
142        cls.da_method = cls.__name__
143
144        return cls
145    return dataclass_with_defaults

Turn a dataclass-style class into a DA method for DAPPER (xp).

This decorator applies to classes that define DA methods. An instances of the resulting class is referred to (in DAPPER) as an xp (short for experiment).

The decorated classes are defined like a dataclass, but are decorated by @da_method() instead of @dataclass.

The classes must define a method called assimilate. This method gets slightly enhanced by this wrapper which provides:

  • Initialisation of the Stats object, accessible by self.stats.
  • fail_gently functionality.
  • Duration timing
  • Progressbar naming magic.

Example:

>>> @da_method()
... class Sleeper():
...     "Do nothing."
...     seconds : int  = 10
...     success : bool = True
...     def assimilate(self, *args, **kwargs):
...         for k in range(self.seconds):
...             time.sleep(1)
...         if not self.success:
...             raise RuntimeError("Sleep over. Failing as intended.")

Internally, da_method is just like dataclass, except that adds an outer layer (hence the empty parantheses in the above) which enables defining default parameters which can be inherited, similar to subclassing.

Example:

>>> class ens_defaults:
...     infl : float = 1.0
...     rot  : bool  = False
>>> @da_method(ens_defaults)
... class EnKF:
...     N     : int
...     upd_a : str = "Sqrt"
...
...     def assimilate(self, HMM, xx, yy):
...         ...

Apart from what's listed in the above Note, there is nothing special to the resulting xp. That is, just like any Python object, it can serve as a data container, and you can write any number of attributes to it (at creation-time, or later). For example, you can set attributes that are not used by the assimilate method, but are instead used to customize other aspects of the experiments (see dapper.xp_launch.run_experiment).