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:
Define the simulation model, observation operator, and error models.
Register these components in an inverse problem (Filter or Smoother).
Create an OED problem that wraps the inverse problem and an optimality criterion.
Register an optimizer with the OED problem.
Solve – the optimizer iterates over candidate designs, evaluating the criterion (which internally solves the inverse problem) at each step.
Inspect the
OEDResultsreturned bysolve().
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
dictor*Configsinterchangeably.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— wrapsscipy.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 |
|---|---|
|
|
|
|
|
|
|
|
|
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— whenTrue, observation vectors are projected onto the active sensor subspace defined by the current design.