`GoldenSearch` returns wrong results for any bracket not straddling zero

Open Beginner friendly
#236 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
Under an hour
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
backend

Research direction

Start in optimistix/_solver/golden.py at GoldenSearch.init(), then run the minimal reproduction from the issue with a bracket that does not straddle zero. Confirm the initial midpoint stays within the supplied bracket and that the existing solver converges successfully for both shifted and zero-centered brackets.

Written by the indexing model from the issue text.

Description

Summary

optimistix.GoldenSearch computes its initial midpoint incorrectly in
GoldenSearch.init(). The bug is masked whenever the search bracket
[lower, upper] straddles zero (lower < 0 < upper), which is why it's easy
to miss in casual testing, but it produces wrong results — often
outside the bracket — for any bracket that doesn't straddle zero
(e.g. [99, 101], or any bracket from a physically-motivated problem where
the variable of interest isn't centered near 0).

Minimal reproduction
import jax.numpy as jnp
import optimistix as optx

def f(x, args):
    return (x - 100.0) ** 2  # true minimum at x=100

solver = optx.GoldenSearch(rtol=1e-4, atol=1e-4)

# Bracket [99, 101] -- does NOT straddle zero
sol = optx.minimise(
    f, solver, y0=jnp.array(100.0), args=None,
    options={"lower": jnp.array(99.0), "upper": jnp.array(101.0)},
    max_steps=64, throw=False,
)
print(sol.value, sol.result)
# -6286.344418537492  RESULTS<max_steps reached ...>
# Expected: something close to 100.0, well inside [99, 101].

# Same function, bracket [-1, 1] shifted so the minimum sits at 0 instead
def g(x, args):
    return x ** 2

sol2 = optx.minimise(
    g, solver, y0=jnp.array(0.0), args=None,
    options={"lower": jnp.array(-1.0), "upper": jnp.array(1.0)},
    max_steps=64, throw=False,
)
print(sol2.value, sol2.result)
# 0.0001322139224121388  RESULTS<successful>
# Converges correctly -- only difference is the bracket straddles zero.
Root cause

In optimistix/_solver/golden.py, GoldenSearch.init():

golden_ratio = (1 + math.sqrt(5)) / 2
middle = (upper - lower) / (golden_ratio + 1)

middle is used throughout the rest of the solver (step(), terminate())
as an absolute coordinate within [lower, upper] — it's passed directly
to fn(middle, args) on the very next line, and later compared against y_
(also an absolute coordinate) via is_lower = y_ < state.middle.

But (upper - lower) / (golden_ratio + 1) is only the offset from lower
— the width of the shorter of the two golden-ratio segments — not a point
within [lower, upper]. The lower + term is missing:

middle = lower + (upper - lower) / (golden_ratio + 1)

For a bracket that happens to straddle zero symmetrically (e.g. [-1, 1]),
the buggy value and the correct value are both coincidentally inside the
bracket (0.764 vs. the correct -0.236 for [-1, 1]), so the algorithm
still converges — just to a different, "wrong" first midpoint than intended,
which happens not to matter because both are still valid interior points to
start bisecting from. For any bracket not centered near zero, the buggy
middle lands outside the bracket entirely (e.g. for [99, 101]:
(101-99)/2.618 ≈ 0.764, nowhere near [99, 101]), and every subsequent
step() compounds the error, since step()'s y_ = state.lower + (state.upper - state.middle) uses this already-wrong middle.

Suggested fix
  golden_ratio = (1 + math.sqrt(5)) / 2
- middle = (upper - lower) / (golden_ratio + 1)
+ middle = lower + (upper - lower) / (golden_ratio + 1)

One line. Verified locally (monkeypatching just this line) that it fixes
both the minimal repro above and a real, non-toy physics optimization we
were using GoldenSearch for for (bracket ~[-0.211, -0.201], several
independent test points) — all previously-divergent cases converged
correctly in 8-10 steps each after the fix.

Environment
  • optimistix: 0.1.0
  • jax: 0.10.2
  • equinox: 0.13.8
  • python: 3.13.14
Dominant language
Python
Stars
623
Forks
54
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from patrick-kidger/optimistix

All issues in patrick-kidger/optimistix

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.