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
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 byself.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
).