Source code for romtools.workflows.inverse.full_covariance_lagged_newton
"""Lagged-curvature support for full-covariance natural Newton VI/MF-VI.
This layer extends :mod:`full_covariance_newton` with the same accepted-state
exponential Hessian averaging used by the diagonal Newton implementation.
Rejected line-search candidates do not update the running Hessian.
"""
from __future__ import annotations
from contextlib import contextmanager
import copy
import numpy as np
from romtools.workflows.inverse import full_covariance_newton as _base
from romtools.workflows.inverse.vi_optimization_methods import (
VINewtonOptimizerConfig,
_normalize_newton_curvature_strategy,
)
def _validate_newton_config(optimizer_config) -> VINewtonOptimizerConfig:
if optimizer_config is None:
config = VINewtonOptimizerConfig()
elif not isinstance(optimizer_config, VINewtonOptimizerConfig):
raise TypeError(
"optimizer_config for optimizer_method='newton' must be "
"VINewtonOptimizerConfig"
)
else:
config = copy.deepcopy(optimizer_config)
strategy = _normalize_newton_curvature_strategy(
config.newton_curvature_strategy
)
if strategy not in ("same_sample", "lagged"):
raise NotImplementedError(
"Full-covariance Newton currently supports "
"newton_curvature_strategy='same_sample' or 'lagged'."
)
if strategy == "lagged":
beta = float(config.newton_hessian_averaging_factor)
if not np.isfinite(beta) or not 0.0 <= beta < 1.0:
raise ValueError(
"newton_hessian_averaging_factor must be finite and in [0, 1)."
)
return config
@contextmanager
def _accepted_state_hessian_averaging(config, estimator_name: str):
"""Average raw Hessians only when their evaluated state is accepted.
The full-covariance drivers evaluate Newton information for line-search
candidates before knowing whether the candidate will be accepted. We form
a prospective exponentially averaged Hessian for every candidate, but only
commit it when the driver subsequently asks for that state's gradient norm;
that call occurs for the initial state and after an accepted line-search
candidate, not for rejected candidates.
"""
strategy = _normalize_newton_curvature_strategy(
config.newton_curvature_strategy
)
if strategy == "same_sample":
yield
return
beta = float(config.newton_hessian_averaging_factor)
original_estimator = getattr(_base, estimator_name)
original_natural_gradient_norm = _base._natural_gradient_norm
running_hessian = None
pending_hessian_id = None
pending_running_hessian = None
def averaged_estimator(*args, **kwargs):
nonlocal pending_hessian_id, pending_running_hessian
current = np.asarray(original_estimator(*args, **kwargs), dtype=float)
if running_hessian is None:
prospective = current.copy()
else:
prospective = beta * running_hessian + (1.0 - beta) * current
prospective = 0.5 * (prospective + prospective.T)
pending_hessian_id = id(prospective)
pending_running_hessian = prospective.copy()
return prospective
def commit_gradient_norm(state):
nonlocal running_hessian, pending_hessian_id, pending_running_hessian
state_hessian = state.get("newton_hessian_full")
if (
state_hessian is not None
and pending_hessian_id is not None
and id(state_hessian) == pending_hessian_id
):
running_hessian = pending_running_hessian.copy()
pending_hessian_id = None
pending_running_hessian = None
return original_natural_gradient_norm(state)
setattr(_base, estimator_name, averaged_estimator)
_base._natural_gradient_norm = commit_gradient_norm
try:
yield
finally:
setattr(_base, estimator_name, original_estimator)
_base._natural_gradient_norm = original_natural_gradient_norm
[docs]
def run_vi(*args, **kwargs):
"""Run full-covariance VI with same-sample or lagged Newton curvature."""
config = _validate_newton_config(kwargs.get("optimizer_config"))
call_kwargs = dict(kwargs)
call_kwargs["optimizer_method"] = "gradient"
call_kwargs["optimizer_config"] = _base._gradient_config_from_newton(config)
with _accepted_state_hessian_averaging(config, "estimate_ordinary_hessian"):
with _base._patched_single_fidelity_newton(config):
return _base._fc_vi.run_vi(*args, **call_kwargs)
[docs]
def run_mf_vi(*args, **kwargs):
"""Run full-covariance MF-VI with same-sample or lagged Newton curvature."""
config = _validate_newton_config(kwargs.get("optimizer_config"))
call_kwargs = dict(kwargs)
call_kwargs["optimizer_method"] = "gradient"
call_kwargs["optimizer_config"] = _base._gradient_config_from_newton(config)
call_kwargs["rom_substep_newton_config"] = config
with _accepted_state_hessian_averaging(
config, "estimate_mf_ordinary_hessian"
):
with _base._patched_mf_newton(config):
return _base._fc_mf.run_mf_vi(*args, **call_kwargs)
[docs]
def mf_vi_with_auto_rom(*args, **kwargs):
"""Run auto-ROM full-covariance MF-VI with lagged-capable Newton."""
original_run_mf_vi = _base._fc_auto.run_mf_vi
_base._fc_auto.run_mf_vi = run_mf_vi
try:
return _base._fc_auto.mf_vi_with_auto_rom(*args, **kwargs)
finally:
_base._fc_auto.run_mf_vi = original_run_mf_vi