Architecture Overview#

This page describes the high-level architecture of PyOED, including module dependencies, data flow, and key design patterns.

Module Dependency Graph#

The dependency order (bottom to top) is:

pyoed.utility          (no internal deps)
pyoed.configs          (depends on utility)
pyoed.models           (depends on configs, utility)
pyoed.stats            (depends on configs, utility)
pyoed.optimization     (depends on configs, utility)
pyoed.assimilation     (depends on models, optimization, configs)
pyoed.oed              (depends on assimilation, optimization, configs)
pyoed.ml               (depends on utility)

Lazy imports in pyoed/__init__.py (via __getattr__) ensure that importing pyoed is lightweight; submodules are loaded only when first accessed.

Data Flow#

A typical OED workflow follows this data flow:

  1. Define the simulation model, observation operator, and error models.

  2. Register these components in an inverse problem (Filter or Smoother).

  3. Create an OED problem that wraps the inverse problem and an optimality criterion.

  4. Register an optimizer with the OED problem.

  5. Solve – the optimizer iterates over candidate designs, evaluating the criterion (which internally solves the inverse problem) at each step.

  6. Inspect the OEDResults returned by solve().

Design Patterns#

Configuration-Driven Objects#

Every major class pairs with a *Configs dataclass. The set_configurations() decorator binds them together.

Defining a new PyOED class:

from dataclasses import dataclass
from pyoed.configs import PyOEDObject, PyOEDConfigs, set_configurations

@dataclass(kw_only=True, slots=True)
class MyModelConfigs(PyOEDConfigs):
    nx: int = 10          # grid size (default 10)
    random_seed: int = 0  # RNG seed

@set_configurations(MyModelConfigs)
class MyModel(PyOEDObject):
    def __init__(self, configs=None):
        configs = self.configurations_class.data_to_dataclass(configs)
        super().__init__(configs)

Using the class:

# All three are equivalent:
m1 = MyModel()                                     # defaults
m2 = MyModel(configs=dict(nx=20, random_seed=7))   # from dict
m3 = MyModel(configs=MyModelConfigs(nx=20, random_seed=7))  # from dataclass

# Reconfigure after creation:
m1.update_configurations(nx=20)

# Inspect all settings:
print(m1.configurations)
print(MyModel.default_configurations)

This pattern enables:

  • Instantiation from dict or *Configs interchangeably.

  • Runtime validation via validate_configurations.

  • Introspection via default_configurations.

Results Pattern#

Operations that produce output (optimization, filtering, smoothing, OED) return typed *Results dataclass objects (derived from PyOEDData). This provides a uniform interface:

results = oed.solve()
print(results.fun)           # scalar criterion value at optimum
print(results.x)             # raw optimal design vector
d = results.asdict()         # serialize to plain dict

Key result classes:

  • OptimizerResults — optimizer output.

  • ScipyOptimizerResults — wraps scipy.optimize.OptimizeResult.

Abstract Base Classes#

Core abstractions use Python’s ABC mechanism. Derived classes implement the abstract methods while inheriting shared validation, registration, and configuration logic.

from pyoed.models import SimulationModel

class MyForwardModel(SimulationModel):
    def solve_forward(self, parameter):
        """Map parameter → state."""
        ...

    def solve_adjoint(self, state, adjoint_rhs):
        """Map adjoint source → adjoint solution (needed for 4D-Var)."""
        ...

Key ABCs and what to implement:

Abstract class

Methods to implement

SimulationModel

solve_forward, solve_adjoint (optional for 4D-Var)

ObservationOperator

apply, Jacobian_T_matvec

ErrorModel

generate_noise, sample, covariance_matvec, covariance_inv_matvec

Optimizer

solve

OED

evaluate_criterion, grad_design (optional)

Global Settings#

pyoed.configs.SETTINGS is a singleton that controls project-wide behaviour:

import pyoed.configs as cfg

cfg.SETTINGS.RANDOM_SEED = 42
cfg.SETTINGS.OUTPUT_DIR  = '/tmp/pyoed_results'
cfg.SETTINGS.PROJECT_ONTO_ACTIVE_DESIGN_SPACE = True

Available settings:

  • OUTPUT_DIR — base output directory for saved results.

  • RANDOM_SEED — global random seed (None = non-reproducible).

  • VERBOSE — default verbosity for all objects.

  • DEBUG — enable extra consistency checks globally.

  • PROJECT_ONTO_ACTIVE_DESIGN_SPACE — when True, observation vectors are projected onto the active sensor subspace defined by the current design.