This repository contains code for running the TEAMS algorithm, as well as more general ensemble-generating procedures for dynamical systems. Among the included examples are the Frierson GCM featured in Finkel & O'Gorman (2025), as well as the Lorenz-96 system featured in Finkel & O'Gorman (2024). The summary below is meant to convey the general design of the codebase, which aims to be extensible to new algorithms and new dynamical systems. But the implementation is not professional, and will certainly need case-by-case modification. Interested readers are highly encouraged to contact Justin Finkel (justinfocus12@gmail.com) for guidance on using it. Still, we hope that the core ideas conveyed here, in the publications, and in the code structure will help to facilitate community uptake of our methods.
- The driver code hierarchy: dynamical systems and algorithms for running them in ensemble mode
-
DynamicalSystemclass (dynamicalsystem.py): specification of required methods for defining any new system, the most important one beingrun_trajectory. This method takes four arguments beyond the implicit "self" parameter (self gives the rule for stepping forward in time).- The first argument, "icandf", stands for "initial conditions and forcing", and contains all the needed information to (deterministically) provide the initial condition and generate noise, be it in the form of a one-off kick to the initial condition, a sequence of such perturbations, or an integer seed for generating a white noise sequence. All these versions can be found in the Forcing class (forcing.py), and one can implement more versions of forcing such as Levy noise if one wanted to. Because an icandf dictionary must be supplied to every run, any concrete implementation of DynamicalSystem must implement its own
generate_default_icandfmethod. The role of this "icandf" dictionary is crucial, as it provides the extra input necessary beyond the evolution rule The same file implements "ODESystem" and "SDESystem" for ordinary and stochastic differential equations. An SDESystem instance has an ODESystem instance as an instance variable(e.g., the SDE$dX = -aX dt + b dW$ , has the ODE$dx/dt = -ax$ as an instance variable). Both of those must be specified further for particular systems, such as Lorenz-96 as done in the examples. If ">" means "is a superclass of", DynamicalSystem > SDESystem > Lorenz96. For the Frierson GCM, more customization is necessary, and so theFriersonGCMclass implemented inexamples/frierson_gcmdirectly extends DynamicalSystem: DynamicalSystem > FriersonGCM.
- The second argument is
obs_fun: a function handle for any observable of special interest that we should evaluate online while running the model. Technically mandatory, but you could just have it return None. - The third argument is "saveinfo": a dictionary that specified how to save out the data, including possibly the initial condition and different fields in different output files. Its structure is different for each dynamical system.
- The fourth argument is
root_dir: the directory relative to which all output data is stored. This is helpful if one needs to migrate the entire ensemble from one location to another and only have to change one variable in the objects saved to disk.
- The first argument, "icandf", stands for "initial conditions and forcing", and contains all the needed information to (deterministically) provide the initial condition and generate noise, be it in the form of a one-off kick to the initial condition, a sequence of such perturbations, or an integer seed for generating a white noise sequence. All these versions can be found in the Forcing class (forcing.py), and one can implement more versions of forcing such as Levy noise if one wanted to. Because an icandf dictionary must be supplied to every run, any concrete implementation of DynamicalSystem must implement its own
- Ensemble class (ensemble.py) specifies a data structure for managing large, and growing, ensembles of simulations stored on the disk. It uses a networkx graph to represent relationships between ensemble members, and is agnostic to the type of dynamical system. Any Ensemble instance has a DynamicalSystem instance as an attribute. Any call to
DynamicalSystem.run_trajectorymust go through the Ensemble instance that contains the DynamicalSystem instance, via thebranch_or_plantmethod. Note there is a lot of dendromorphizing in the nomenclature (that's anthropomorphizing, but with tree-like attributes instead of human-like attributes). - EnsembleAlgorithm class (algorithms.py): in general, an EnsembleAlgorithm is a rule for generating the next member in an Ensemble given its current state. TEAMS is one example; DNS is another example with a very simple rule of extending the previous simulation chunk, picking it up at the endpoint; and "PeriodicBranching" is another example that we used to calculate ensemble dispersion rates in Finkel and O'Gorman (2025). Each of these is specified as a still-abstract subclass in the algorithms.py file, and the concrete implementations are completed in the examples subfolder. E.g., FriersonGCMTEAMS < TEAMS < EnsembleAlgorithm, and Lorenz96TEAMS < SDETEAMS < TEAMS < EnsembleAlgorithm.
-
- The Lorenz96 system, implemented in examples/lorenz96, is a good example to follow if implementing an algorithm on a new SDESystem.
- The Frierson GCM
- As a DynamicalSystem object, it is implemented as
FriersonGCMinexamples/frierson_gcm/frierson_gcm.py. The core methods likerun_trajectorymerely wrap, via system calls, a large Fortran codebase that exists in some other directory, which is specified in a "config" dictionary, which must be supplied when creating an instance of FriersonGCM (see__init__method). See thedefault_configmethod for a template. In our case, that codebase is in the companion repositoryjf_conv_gray_smooth, on thespptbranch. I will not attempt to describe that repository in detail --- it comes packaged with professional documentation from GFDL --- but point out that the main interface with this model happens in a few important methods:-
compile_mppnccombinecompiles the code for converting output to netcdf -
generate_default_icandfdemonstrates the two possible forcing choices: SPPT (which we used in Finkel & O'Gorman 2025), and impulsive (INT) forcing. -
default_config: Here you can adjust things like the resolution (T21orT42, which will automatically result in updates to the namelist) -
derive_parameters: set some instance variables following the config dictionary
-
- Within
examples/frierson_gcm, the filealgorithms_frierson.pyimplements concrete versions of abstract Algorithms classes, most relevantly FriersonGCMTEAMS < TEAMS, FriersonGCMPeriodicBranching < PeriodicBranching, and FriersonGCMDirectNumericalSimulation < DirectNumericalSimulation. For the former two, one of the inputs to__init__areinit_conds(restart files) andinit_times(integer timestamps). There are various factory methodsinitialize_from...that take in a "dns" parameter which takes the form of aFriersonGCMDirectNumericalSimulationinstance, and find appropriate initial conditions from that DNS.- DNS on the Frierson GCM: the script
dns_frierson.pyactually runs and analyzes DNS. The__main__block looks for a command line argument which specifies the "procedure": either "single" (run the GCM with a single set of physical parameters), or "meta" (compare a collection of completed runs across different parameters). In the case of "single", a second argument is expected: an integer indexi_exptspecifying which parameter value to use. The functiondns_multiparamscreates the multidimensional array of parameters, andi_exptcodes for a flat index (row-major / C-style ordering). In its present form, three of those parameters are singletons and only "sigmas" is a variable parameter; these are the noise strengths for SPPT. The other scripts (pebr_frierson.pyandteams_frierson.py) have a similar setup, but with genuinely multidimensional parameter sets, e.g., advance split time and target field. Converting the flat index to a multi-index facilitates running these experiments in large batches on SLURM. A call looks like:$ python dns_frierson.py single 4where the "4" specifies to usesigma=0.3. - TEAMS on the FriersonGCM: the script
teams_frierson.pyruns and analyzes TEAMS. The__main__block has four procedures:single(a single run with a single seed),multiseed(aggregate analysis of many independent runs with the same parameters and different seeds, e.g., return period plots), andmultidelta(analysis of performance across different advance split times). They should be executed in that order, and all after DNS so there are initial conditions to draw ancestors from. A call looks like:$ python teams_frierson.py single 281(281 is arbitrary, but represents a flattened index in the space of target variable {precip, temperature} x advance split time {0, 4, 6, ..., 20} x seed {0, 1, ..., 47}. - PeriodicBranching on the Frierson GCM: the script
pebr_frierson.pyruns and analyzes PeriodicBranching. Here, the 'meta' procedure is outdated and needs updating. A call looks like$ python pebr_frierson.py single 4The scripts described above share a common procedural structure, where<algorthmname>_single_procedurefunctions have a dictionarytododictof tasks assigned 0 or 1, so that you can run different stages in the pipeline in isolation. This is my way of implementing a notebook-like workflow in a script.
- DNS on the Frierson GCM: the script
- As a DynamicalSystem object, it is implemented as