Skip to content

A few EnOpt updates (scaling and multilevel gradient fix) - #193

Merged
rolfjl merged 2 commits into
Python-Ensemble-Toolbox:mainfrom
rolfjl:main
Nov 11, 2026
Merged

rolfjl merged 2 commits into
Python-Ensemble-Toolbox:mainfrom
rolfjl:main

Conversation

@rolfjl

@rolfjl rolfjl commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

No description provided.

rolfjl and others added 2 commits November 9, 2026 10:50
In a multilevel EnOpt with a coarse gradient and fine trial steps (en_size =
[ne, 0]) the ensemble gradient subtracted the fine objective at the mean from the
coarse members. Perturbations are recentred and then clipped, so for a control at
a bound the clipped perturbations have a non-zero mean and the coarse-fine offset
appeared as a gradient component on every bound control. Normalised by its
largest element, the gradient then spent the step on the bound controls (clipped
away) and moved the free controls by a fraction of alpha. The Froy mixed run
crawled at 0.02-0.03 per iteration because of this.

- function(x, level=...) chooses the fidelity level of a single control vector
  (default: the finest level; a single state used to fail calc_prediction's list
  assertion and before that ran on the simulator's default level -1).
  A value on a coarser level is returned but does not update stateF.
- GaussianEnsemble.gradient evaluates the mean state once on each coarser level
  with members and uses it as that level's reference; the Hessian shares it.
  If that evaluation is not finite, the mean of the level's finite members is
  used and a warning is logged. Level weights accept one weight per level or per
  populated level.
- calc_prediction counts members per level from the state instead of en_size
  (en_size need not add up to ne) and records run_levels; saved predictions use
  those levels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-cube transform

The ensemble gradient is Cov(F, x), a covariance-preconditioned gradient, so in
unit-cube coordinates u = (x - lb) / db it is Cov(F, x) / db. The optimizer wrapped
jac with the derivative rule (times db) and the Hessian with db x db, so every
component was off by db^2 and controls with wide bounds dominated the normalised
step: with injector ranges of 8000 and producer ranges of 5000 the injectors were
weighted 2.56 times more than in the ensemble's own unit-cube formulation.

- BoundTransformHandler gets natural_jac/hess_to/from_unit_cube (divide by the
  range, and by range x range).
- EnOpt wraps jac and hess with those rules when the gradient is natural, decided
  by the new option natural_gradient, defaulting to the setting of the ensemble
  the jac is bound to (else True). The covariance update and the reported jac and
  hess use the matching inverse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rolfjl
rolfjl merged commit c65f4a0 into Python-Ensemble-Toolbox:main Nov 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant