Analytic sine VI and MF-VI examples#
These examples use the analytically tractable sine-series inverse problem from the VI/MF-VI benchmark. The forward model is
with
so the exact solution is
The prior is
with synthetic truth
and observation-noise standard deviation \(\sigma_\eta=5\times10^{-3}\). Because the forward map is linear and the prior and likelihood are Gaussian, the exact posterior is available in closed form,
This lets both examples measure convergence directly against the exact posterior.
VI-paper benchmark configuration#
The diagonal example uses the Newton and multifidelity settings from the VI-paper analytic-sine benchmark. The production run uses a full Hessian, lagged stochastic curvature with averaging factor \(\beta=0.25\), natural coordinates, and the paper line-search scales:
optimizer = VINewtonOptimizerConfig(
newton_metric="natural",
newton_hessian_type="full",
newton_curvature_strategy="lagged",
newton_hessian_averaging_factor=0.25,
newton_regularization=5e-4,
gradient_norm_tolerance=0.0,
max_iterations=1000,
)
line_search = VIStochasticNonmonotoneLineSearchConfig(
initial_step_size=0.25,
max_step_size=1.0,
)
The shared VI/MF-VI arguments use the joint entropy estimator, a leave-one-out baseline, zero observation-covariance regularization, and inference seed 7. MF-VI uses a scalar multifidelity control-variate coefficient in both examples:
common_arguments = dict(
...,
optimizer_method="newton",
optimizer_config=optimizer,
line_search_method="stochastic_nonmonotone",
line_search_config=line_search,
baseline_method="loo",
score_function_entropy_strategy="joint",
covariance_regularization=0.0,
random_seed=7,
)
The full diagonal run uses 16 FOM samples per iteration. MF-VI uses 64 additional ROM samples, the automatic GP ROM, and four iterations of ROM training history:
workflows.mf_vi_with_auto_rom(
...,
fom_sample_size=16,
rom_extra_sample_size=64,
max_rom_training_history=4,
mfmc_control_variate_mode="scalar",
rom_type="gp",
rom_args={
"normalize_parameters": True,
"normalize_targets": True,
},
)
prior_parameter_space defines the Bayesian prior, while the required
initial_variational_parameter_space independently selects the variational
family and supplies its initial moments.
Example 1: equispaced observations and a diagonal posterior#
The first example uses \(N=31\) equispaced interior observations,
For these points the discrete sine basis is orthogonal, so \(G^T\Gamma_{\rm obs}^{-1}G\) is diagonal. Since the prior covariance is also diagonal, the exact posterior covariance is diagonal to numerical precision.
The variational initializer is a GaussianParameterSpace. This selects the
diagonal/mean-field VI implementation. This is the case that corresponds
directly to the VI-paper analytic-sine benchmark and therefore uses the exact
configuration listed above.
Run the example with
python examples/vi_mf_vi_demo/diagonal_example.py
Convergence#
The convergence plot reports the exact Gaussian \(D_{\rm KL}(q\|p_{\rm post})\) for the accepted VI and MF-VI states.
VI and MF-VI convergence to the exact diagonal posterior.#
Posterior mean#
Final VI and MF-VI posterior means compared with the exact posterior mean and the synthetic truth.#
Posterior#
Exact, VI, and MF-VI marginal posterior densities for the seven sine coefficients. The vertical dashed line in each panel is the synthetic truth.#
Implementation#
1"""VI versus MF-VI for the diagonal analytic sine posterior."""
2
3from __future__ import annotations
4
5import argparse
6from pathlib import Path
7import shutil
8import sys
9from typing import Optional
10
11import numpy as np
12
13
14REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
15if str(REPOSITORY_ROOT) not in sys.path:
16 sys.path.insert(0, str(REPOSITORY_ROOT))
17
18import romtools.workflows as workflows
19from romtools.workflows.inverse.vi_optimization_methods import (
20 VINewtonOptimizerConfig,
21 VIStochasticNonmonotoneLineSearchConfig,
22)
23from romtools.workflows.parameter_spaces import GaussianParameterSpace, MonteCarloSampler
24
25from examples.vi_mf_vi_demo.analytic_sine_support import (
26 AnalyticSineQoiModel,
27 PRIOR_STD,
28 build_problem,
29 collect_history,
30 write_convergence_plot,
31 write_diagonal_posterior_plot,
32 write_mean_plot,
33)
34
35
36def main(
37 smoke: bool = False,
38 work_dir: Optional[str] = None,
39 output_dir: Optional[str] = None,
40) -> None:
41 """Run the equispaced-observation diagonal-posterior example."""
42 problem = build_problem(equispaced=True)
43 model = AnalyticSineQoiModel(problem)
44
45 sample_size = 8 if smoke else 16
46 rom_extra_sample_size = 8 if smoke else 64
47 max_iterations = 2 if smoke else 1000
48
49 prior = GaussianParameterSpace(
50 parameter_names=list(problem.parameter_names),
51 means=problem.prior_mean,
52 stds=np.full(problem.prior_mean.size, PRIOR_STD),
53 sampler=MonteCarloSampler,
54 )
55 initial_variational_parameter_space = GaussianParameterSpace(
56 parameter_names=list(problem.parameter_names),
57 means=problem.prior_mean,
58 stds=np.full(problem.prior_mean.size, PRIOR_STD),
59 sampler=MonteCarloSampler,
60 )
61 optimizer = VINewtonOptimizerConfig(
62 newton_metric="natural",
63 newton_hessian_type="full",
64 newton_curvature_strategy="lagged",
65 newton_hessian_averaging_factor=0.25,
66 newton_regularization=5e-4,
67 gradient_norm_tolerance=0.0,
68 max_iterations=max_iterations,
69 )
70 line_search = VIStochasticNonmonotoneLineSearchConfig(
71 initial_step_size=0.25,
72 max_step_size=1.0,
73 )
74
75 root = (
76 Path(work_dir).resolve()
77 if work_dir
78 else Path(__file__).parent / "diagonal_work"
79 )
80 shutil.rmtree(root, ignore_errors=True)
81 vi_dir = root / "vi"
82 mf_vi_dir = root / "mf_vi"
83
84 common_arguments = dict(
85 model=model,
86 prior_parameter_space=prior,
87 initial_variational_parameter_space=initial_variational_parameter_space,
88 observations=problem.observations,
89 observations_covariance=problem.observation_covariance,
90 optimizer_method="newton",
91 optimizer_config=optimizer,
92 line_search_method="stochastic_nonmonotone",
93 line_search_config=line_search,
94 baseline_method="loo",
95 score_function_entropy_strategy="joint",
96 bounded_parameter_handling="clip",
97 covariance_regularization=0.0,
98 random_seed=7,
99 restart_files_to_keep=max_iterations + 1,
100 )
101
102 workflows.run_vi(
103 **common_arguments,
104 absolute_work_dir=str(vi_dir),
105 sample_size=sample_size,
106 evaluation_concurrency=1,
107 )
108 workflows.mf_vi_with_auto_rom(
109 **common_arguments,
110 absolute_work_dir=str(mf_vi_dir),
111 fom_sample_size=sample_size,
112 rom_extra_sample_size=rom_extra_sample_size,
113 fom_evaluation_concurrency=1,
114 rom_evaluation_concurrency=1,
115 max_rom_training_history=4,
116 mfmc_control_variate_mode="scalar",
117 rom_type="gp",
118 rom_args={
119 "normalize_parameters": True,
120 "normalize_targets": True,
121 },
122 )
123
124 vi_history = collect_history(vi_dir, problem)
125 mf_history = collect_history(mf_vi_dir, problem)
126 output = (
127 Path(output_dir).resolve()
128 if output_dir
129 else Path(__file__).parent
130 )
131 write_convergence_plot(
132 output / "analytic_sine_diagonal_convergence.png",
133 vi_history,
134 mf_history,
135 "Equispaced observations: convergence to the exact posterior",
136 )
137 write_mean_plot(
138 output / "analytic_sine_diagonal_mean.png",
139 problem,
140 vi_history,
141 mf_history,
142 "Equispaced observations: posterior mean",
143 )
144 write_diagonal_posterior_plot(
145 output / "analytic_sine_diagonal_posterior.png",
146 problem,
147 vi_history,
148 mf_history,
149 )
150
151
152def _parse_args():
153 parser = argparse.ArgumentParser(description=__doc__)
154 parser.add_argument("--smoke", action="store_true")
155 parser.add_argument("--work-dir")
156 parser.add_argument("--output-dir")
157 return parser.parse_args()
158
159
160if __name__ == "__main__":
161 arguments = _parse_args()
162 main(
163 smoke=arguments.smoke,
164 work_dir=arguments.work_dir,
165 output_dir=arguments.output_dir,
166 )
Regenerating the figures#
Run both production examples and regenerate all eight figures with
python examples/vi_mf_vi_demo/generate_docs_media.py
A reduced CI configuration exercises both examples with
python examples/vi_mf_vi_demo/example.py --smoke
ROM-only optimizer substeps#
MF-VI can optionally perform a fixed number of inexpensive ROM-only
optimization updates between FOM-anchored iterations, mirroring the MF-EKI
substep window.
For example, add the following keyword arguments to run_mf_vi or
mf_vi_with_auto_rom:
rom_substep_start_iteration=0,
rom_substep_end_iteration=20,
num_rom_substeps=2,
The defaults are 0, None, and 0, respectively; therefore ROM
substeps are disabled unless requested. The start index is inclusive and the
end index exclusive, counting only outer MF-VI iterations.
Each ROM-only substep uses the same optimization method and configuration as the outer MF-VI iteration. Gradient descent uses the configured standard or natural gradient; Adam uses the configured learning rate, Fisher damping, clipping, and moment parameters. Adam initializes a fresh optimizer at the start of every ROM-only sequence, reuses its moments within that sequence, and never changes the outer Adam moments. Newton substeps recompute both a fresh gradient and Hessian and use the configured metric, curvature type, and regularization. All inner updates respect the existing covariance and step-limiting safeguards.
The ROM remains frozen during substeps. The final proposed distribution is evaluated by the existing multifidelity FOM/ROM candidate check and rejected as a whole if its outer line search fails. ROM-only substeps do not perform FOM evaluations, refresh the ROM training set, or independently terminate the solve. They are distinct from the ABRIS sample-reuse batch-refresh loop.
Because the inner optimization direction uses an approximate ROM objective, this acceleration is not guaranteed to reduce FOM work on every problem. A finite end iteration can disable it near convergence if ROM directional errors cause oscillation.