Getting Started with PyOED#

Tip

If you are new to PyOED, we recomend to first experiment with the step-by-step guidance/examples given below, then inspect PyOED’s configurations approach. After that, the user can follow our list of examples (driver scripts) and Notebook (Jupyter Notebooks) Tutorials.

Step by step guide to starting with PyOED

Use Existing PyOED classes#

Nearly all PyOED objects can be configured with maximum flexibility. Specifically, each object (e.g., simulation model) can be instantiated with default configurations (defined by the associated configurations class). After instantiation, one can always call the method update_configurations() associated with each object with one or more key-word arguments defining the required configuations.

In what follows, we will use a simple Lorenz63 model to illustrate the concept of creating, configuring, and reconfiguring PyOED objects.

Creating PyOED objects with default configurations#

Create A Lorenz63 model and simulate it#
 from pyoed.models.simulation_models.lorenz import Lorenz63
 model = Lorenz63()

 ic = model.create_initial_condition()
 tspan, trajectory = model.integrate_state(ic, tspan=(0, 3))

Configure PyOED objects upon instantiation#

Each PyOED class is associated (by construction) with a configurations class. For details on how configurations classes are defined, and how they are associated with each object automatically upon instantiation, see the PyOED configuraitons (configs) guid.

Configuring a PyOED object upon instantiation is carried out by passing the set of configurations either as a dictionary dict object (the easiest), or as an instance of the configurations class associated with the object (more advanced).

Create A Lorenz63 model with configurations passed as dictionary#
 from pyoed.models.simulation_models.lorenz import Lorenz63
 model = Lorenz63(
     configs=dict(rho=30, beta=2, sigma=11, dt=0.02 )
 )

 ic = model.create_initial_condition()
 tspan, trajectory = model.integrate_state(ic, tspan=(0, 3))
Create A Lorenz63 model with configurations passed as configuration object#
 from pyoed.models.simulation_models.lorenz import Lorenz63, Lorenz63Configs
 model = Lorenz63(
     configs=Lorenz63Configs(rho=30, beta=2, sigma=11, dt=0.02 )
 )

 ic = model.create_initial_condition()
 tspan, trajectory = model.integrate_state(ic, tspan=(0, 3))

Reconfigure PyOED objects after instantiation#

In many cases, once can reconfigure (update configurations of) a PyOED object. The example below shows how to one, or multiple settings/configurations of the Lorenz63 simulation model after it is created (by following any of the instantiation methods above).

Create A Lorenz63 model with default configurations and then update its configurations#
 from pyoed.models.simulation_models.lorenz import Lorenz63
 model = Lorenz63()
 model.update_configurations(rho=30, beta=2, sigma=11, dt=0.02 )

Note

  1. The passed configurations are aggregated with (and validated together and against) the existing set of configurations associated with the object.

  2. If the user passes a keyword (configuraiton key) to the update_configurations() method, an Exception (by default a pyoed.configs.PyOEDConfigsValidationError) will be thrown.

Creating Hierarchical Objects#

In data assimilation (inverse problems), a common hierarchy is that an inverse problem requires a simulation model, an observation operator, and a data-noise (observation error) model, and in some algorithms an optimizer. This creates an inherent hierarchy that need to be preserved before objects are fully functional. Thus, the user should create each of these components first, and make sure they are passed in the configurations of the next object.

To learn this hierarchy, simply follow the configurations of each object backward. For example, we want to create and solve an inverse problem defined as follows:

  1. Formulate and solve the problem as a three-dimensional variational (3DVar) data assimilation algorithm

  2. The simulation model: Lorenz63

  3. The prior: Gaussian Prior

  4. The observation operator: Identity

  5. The data-noise model: Gaussian

New PyOED users can follow the following steps:

  1. Identify the data assimilation algorithm implementation. In this case, we need pyoed.assimilation.filtering.threeDVar.VanillaThreeDVar.

  2. Identify the associated configurations class. In this case, pyoed.assimilation.filtering.threeDVar.VanillaThreeDVarConfigs. By convention, each configurations class is defined with the same name as the associated object with a postfix of "Configs". Alternatively, one can check the set_configurations() decorator that preeceeds each class definition.

  1. By inspecting the configurations (each class/object has an attribute default_configurations which shows all configurations with associated default value defined by the associated configurations class), the user can see all configurations required including the model, prior, observation_operator, observation_error_model, and optimizer.

  2. Repeat the process above, for each of those dependencies.

    1. The model, does not depend on any of the other objects.

    2. The observation_error_model requires the model as part of its configurations.

    3. The prior and the observation_error_model require defining the dimension (size), and the parameters (mean and covariance) of the Gaussian distribution which depend on model’s inference parameter/state and the observation, respectively.

Thus, to create the inverse problem, one can do the following:

from pyoed.models.simulation_models.lorenz import Lorenz63
model = Lorenz63()

from pyoed.models.observation_operators.identity import Identity
obs_oper = Identity({'model': model, })

from pyoed.models.error_models.Gaussian import GaussianErrorModel
prior = GaussianErrorModel({'mean': 1, 'size':model.state_size, })

obs_noise = GaussianErrorModel(
    {
       'size': obs_oper.observation_size,
       'variance': 0.01,
    }
)

from pyoed.assimilation.filtering.threeDVar import VanillaThreeDVar
ip = VanillaThreeDVar(
    {
        'model': model,
        'prior': prior,
        'observation_operator': obs_oper,
        'observation_error_model': obs_noise,
        'invert_for': 'state',
    }
)

Note

The code above creates the data assimilation (inverse problem) object, however, if you attempt to solve the inverse problem, an error will be thrown (by default pyoed.configs.PyOEDConfigsValidationError) because no observations have been configured/registered. One can read data from file, or create synthetic observations as in the following snippen which complements the code above

Tip

To solve the inverse problem, simply call the method solve() associated with the data assimilation object.

ic = model.create_initial_condition()
y = obs_oper(ic)
y = obs_noise.add_noise(y)
ip.update_configurations(observations=y)

results = ip.solve()