Quick Start#

This page shows a complete, self-contained Bayesian sensor-placement OED workflow using PyOED’s toy linear model. After working through this example you will understand the four building blocks of every PyOED application: forward model, error models, inverse problem, and OED problem.

Note

Make sure PyOED is installed before running this example. See Download & Install PyOED for instructions. No FEniCSx installation is needed for this example.

Overview#

We want to identify the k best sensors out of n candidate locations for estimating an unknown model parameter. The optimality criterion is Bayesian A-optimality: minimise the trace of the posterior covariance (i.e., minimise total posterior uncertainty).

The five steps are:

  1. Build a toy linear forward model.

  2. Define prior and observation-noise distributions.

  3. Define an observation operator that maps model state to sensors.

  4. Wrap everything in a 3D-Var inverse problem.

  5. Run the sensor-placement OED solver.

Step 1 — Forward Model#

ToyLinearTimeIndependent provides a randomly generated linear map \(y = A\theta + \epsilon\) that is convenient for testing.

import numpy as np
from pyoed.models.simulation_models.toy_linear import ToyLinearTimeIndependent

# 8-dimensional state space, 8-dimensional parameter space
model = ToyLinearTimeIndependent(configs=dict(nx=8, np=8, random_seed=42))

print("State size     :", model.state_size)    # 8
print("Parameter size :", model.parameter_size) # 8

Step 2 — Error Models (Prior and Observation Noise)#

All probability distributions in PyOED derive from ErrorModel.

For Bayesian inversion the prior is \(\theta \sim \mathcal{N}(0, I)\).

For sensor-placement OED the observation noise must be a PrePostWeightedGaussianErrorModel so that the design vector is woven into the covariance during optimisation:

from pyoed.models.error_models.Gaussian import (
    GaussianErrorModel,
    PrePostWeightedGaussianErrorModel,
)

# Prior: N(0, I_8)
prior = GaussianErrorModel(configs=dict(size=8, mean=0.0, variance=1.0))

# Observation noise: design-weighted Gaussian (required for relaxed OED)
obs_noise = PrePostWeightedGaussianErrorModel(
    configs=dict(size=8, mean=0.0, variance=0.01)
)

Step 3 — Observation Operator#

An Identity operator observes all nx grid points. When the OED solver activates a subset of sensors it toggles entries of the operator’s design vector.

from pyoed.models.observation_operators.identity import Identity

obs_op = Identity(configs=dict(model=model))

print("Observation size :", obs_op.observation_size)  # 8

Step 4 — Inverse Problem (3D-Var)#

VanillaThreeDVar combines the model, prior, observation operator, and noise model into a Bayesian inverse problem that the OED criterion will query during optimisation.

from pyoed.assimilation.filtering.threeDVar import VanillaThreeDVar

da = VanillaThreeDVar(configs=dict(
    model=model,
    invert_for='parameter',        # 'parameter' or 'state'
                                   #   'parameter' — infer model parameters θ
                                   #   'state'     — infer model state x
    prior=prior,
    observation_operator=obs_op,
    observation_error_model=obs_noise,
))

# Generate synthetic observations from a known "true" parameter
rng = np.random.default_rng(42)
true_param = rng.standard_normal(model.parameter_size)
true_state  = model.solve_forward(true_param)
y_obs = obs_op.apply(true_state) + obs_noise.generate_noise()

da.register_observations(y_obs)

Step 5 — Sensor-Placement OED#

SensorPlacementBayesianInversionOED optimises the continuous relaxation of the binary design vector \(\zeta \in [0,1]^{n_y}\). Each entry of \(\zeta\) represents the “weight” assigned to the corresponding sensor; the optimal design concentrates weight on the most informative locations.

from pyoed.oed.sensor_placement import SensorPlacementBayesianInversionOED

oed = SensorPlacementBayesianInversionOED(configs=dict(
    inverse_problem=da,
    problem_is_linear=True,     # exploit linearity for faster solves
    optimizer='ScipyOptimizer',
))

# Register the optimality criterion
oed.register_optimality_criterion('Bayesian A-opt')

# Run the optimisation
results = oed.solve()

print("Optimal design  :", np.round(oed.design, 3))
print("Criterion value :", results.fun)

Complete Example#

Copy-paste the snippet below to run the full workflow end-to-end:

import numpy as np
from pyoed.models.simulation_models.toy_linear import ToyLinearTimeIndependent
from pyoed.models.error_models.Gaussian import (
    GaussianErrorModel,
    PrePostWeightedGaussianErrorModel,
)
from pyoed.models.observation_operators.identity import Identity
from pyoed.assimilation.filtering.threeDVar import VanillaThreeDVar
from pyoed.oed.sensor_placement import SensorPlacementBayesianInversionOED

# ── 1. Forward model ────────────────────────────────────────────────────
model = ToyLinearTimeIndependent(configs=dict(nx=8, np=8, random_seed=42))

# ── 2. Error models ──────────────────────────────────────────────────────
prior     = GaussianErrorModel(configs=dict(size=8, mean=0.0, variance=1.0))
obs_noise = PrePostWeightedGaussianErrorModel(
    configs=dict(size=8, mean=0.0, variance=0.01)
)

# ── 3. Observation operator ──────────────────────────────────────────────
obs_op = Identity(configs=dict(model=model))

# ── 4. Inverse problem ───────────────────────────────────────────────────
da = VanillaThreeDVar(configs=dict(
    model=model,
    invert_for='parameter',
    prior=prior,
    observation_operator=obs_op,
    observation_error_model=obs_noise,
))

rng = np.random.default_rng(42)
true_param = rng.standard_normal(model.parameter_size)
true_state  = model.solve_forward(true_param)
y_obs = obs_op.apply(true_state) + obs_noise.generate_noise()
da.register_observations(y_obs)

# ── 5. Sensor-placement OED ──────────────────────────────────────────────
oed = SensorPlacementBayesianInversionOED(configs=dict(
    inverse_problem=da,
    problem_is_linear=True,
    optimizer='ScipyOptimizer',
))
oed.register_optimality_criterion('Bayesian A-opt')
results = oed.solve()

print("Optimal design  :", np.round(oed.design, 3))
print("Criterion value :", results.fun)

What Next?#

Getting Started

Deeper walk-through of every PyOED component with annotated examples.

Getting Started with PyOED
Key Concepts

Mathematical background: Bayesian inversion, A/D/E-optimality, EIG, and design-space projection.

Key Concepts
Examples & Tutorials

Jupyter notebooks for time-dependent inversion, robust OED, reinforcement-learning-based design, and more.

Examples & Tutorials
API Reference

Full class and method documentation for all subpackages.

API Reference