Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions xopt/generators/bayesian/turbo.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,13 @@ def get_trust_region(self, generator: "BayesianGenerator") -> Tensor:
if active_indices:
lengthscales = lengthscales[active_indices]

# calculate the ratios of lengthscales for each axis
# calculate the ratios of lengthscales for each axis,
# taking the per-element root before multiplying so the
# product cannot overflow/underflow for extreme
# lengthscales (e.g. degenerate fits in high dimensions)
active_dim = len(active_variable_names)
weights = lengthscales / torch.prod(lengthscales) ** (
1 / active_dim
weights = lengthscales / torch.prod(
lengthscales ** (1 / active_dim)
)

# calculate the tr bounding box
Expand Down
51 changes: 51 additions & 0 deletions xopt/tests/generators/bayesian/test_turbo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np
import pandas as pd
import pytest
import torch
import yaml

from gest_api.vocs import VOCS
Expand Down Expand Up @@ -594,3 +595,53 @@ def clean_up(self):
for f in files:
if os.path.exists(f):
os.remove(f)


# Lives outside TestTurbo because pytest cannot parametrize unittest.TestCase
# methods -- parametrization is fixture-driven and TestCase methods take no
# fixtures, so the decorator would leave the argument unfilled at call time.
@pytest.mark.parametrize(
"lengthscale",
[pytest.param(1.0e200, id="overflow"), pytest.param(1.0e-200, id="underflow")],
)
def test_get_trust_region_extreme_lengthscales(lengthscale):
# The geometric-mean normalization must survive lengthscales at both
# extremes. With the naive prod(ls) ** (1 / d) form the raw product is
# unrepresentable in either direction, and the trust region degenerates
# silently -- no warning, no NaN, no exception:
# huge ls -> prod overflows to inf -> weights 0 -> region collapses
# to the center point
# tiny ls -> prod underflows to 0 -> weights inf -> region is clamped
# out to the whole domain
# The underflow case is why "finite, with nonzero width" does not pin this
# on its own: a region spanning the entire domain satisfies both.
test_vocs = deepcopy(TEST_VOCS_BASE)
gen = UpperConfidenceBoundGenerator(vocs=test_vocs)
gen.add_data(TEST_VOCS_DATA)
gen.train_model()

gen.model.models[0].covar_module.lengthscale = torch.tensor(
[[lengthscale, lengthscale]], dtype=torch.double
)

turbo_state = OptimizeTurboController(vocs=gen.vocs)
turbo_state.update_state(gen)
tr = turbo_state.get_trust_region(gen)

assert torch.all(torch.isfinite(tr))

# Equal lengthscales normalize to unit weights, so the region spans at most
# `length` of each dimension's full width. Bounding rather than pinning the
# width keeps this robust to the clamp in get_trust_region, which
# legitimately narrows a side when the incumbent sits near a bound.
bounds = torch.tensor(np.array(test_vocs.bounds).T, dtype=torch.double)
widths = tr[1] - tr[0]
max_widths = turbo_state.length * (bounds[1] - bounds[0])

assert torch.all(widths > 0.0), (
f"region collapsed to a point, widths {widths.tolist()}"
)
assert torch.all(widths <= max_widths + 1e-12), (
f"region blown out past the trust region, widths "
f"{widths.tolist()} exceed {max_widths.tolist()}"
)
Loading