Skip to content

Optimization

This page documents 3 public symbols. Each entry includes its purpose, import path, full API docstring, and the maintained example that exercises it.

Conceptual guide

OptimizationResult

Result returned by a one-dimensional optimizer.

Property Value
Type class
Import from pygwrx.core import OptimizationResult
Signature OptimizationResult(value: 'Union[float, int]', score: 'float', iterations: 'int', converged: 'bool', evaluations: 'int' = 0, message: 'str' = '') -> None
Maintained example examples/core/06_optimization.py

OptimizationResult dataclass

OptimizationResult(
    value: Union[float, int],
    score: float,
    iterations: int,
    converged: bool,
    evaluations: int = 0,
    message: str = "",
)

Result returned by a one-dimensional optimizer.

Parameters:

Name Type Description Default
value Union[float, int]

Best parameter value found.

required
score float

Objective-function value at value.

required
iterations int

Number of optimization updates performed.

required
converged bool

Whether the stopping criterion was satisfied and a finite solution was found.

required
evaluations int

Number of unique objective-function evaluations.

0
message str

Human-readable termination message.

''
Notes

The first four fields are retained for backward compatibility with the original project implementation. evaluations and message are additive metadata fields.

GoldenSectionSearch

Golden-section search for one-dimensional minimization.

Property Value
Type class
Import from pygwrx.core import GoldenSectionSearch
Signature GoldenSectionSearch(tol: 'float' = 1e-05, max_iter: 'int' = 100, verbose: 'bool' = True)
Maintained example examples/core/06_optimization.py

GoldenSectionSearch

GoldenSectionSearch(
    tol: float = 1e-05,
    max_iter: int = 100,
    verbose: bool = True,
)

Golden-section search for one-dimensional minimization.

Continuous searches use the standard golden-section interval reduction. Adaptive bandwidth searches use a discrete integer variant and finish by evaluating every integer in the final short bracket. Unlike the original implementation, convergence is controlled by tol rather than by a hard-coded constant or by equality of objective values.

Parameters:

Name Type Description Default
tol float

Positive convergence tolerance for the search interval.

1e-05
max_iter int

Maximum number of interval-reduction updates.

100
verbose bool

Whether to print progress information.

True
Source code in src/pygwrx/core/optimization.py
def __init__(
    self,
    tol: float = 1e-5,
    max_iter: int = 100,
    verbose: bool = True,
):
    self.tol = _validate_positive_float(tol, "tol")
    self.max_iter = _validate_positive_int(max_iter, "max_iter")
    self.verbose = _validate_bool(verbose, "verbose")

minimize

minimize(
    func: Callable[[float], float],
    lower: float,
    upper: float,
    adaptive: bool = False,
) -> OptimizationResult

Minimize a scalar objective on a closed interval.

Parameters:

Name Type Description Default
func Callable[[float], float]

Scalar objective function. Lower scores are better.

required
lower float

Finite lower search bound.

required
upper float

Finite upper search bound with lower <= upper.

required
adaptive bool

If True, search only integer nearest-neighbour counts.

False

Returns:

Name Type Description
OptimizationResult OptimizationResult

Best candidate, objective score, convergence state and metadata.

Source code in src/pygwrx/core/optimization.py
def minimize(
    self,
    func: Callable[[float], float],
    lower: float,
    upper: float,
    adaptive: bool = False,
) -> OptimizationResult:
    """Minimize a scalar objective on a closed interval.

    Args:
        func: Scalar objective function. Lower scores are better.
        lower: Finite lower search bound.
        upper: Finite upper search bound with ``lower <= upper``.
        adaptive: If ``True``, search only integer nearest-neighbour counts.

    Returns:
        OptimizationResult: Best candidate, objective score, convergence state and metadata.
    """
    objective = _validate_objective(func)
    adaptive_value = _validate_bool(adaptive, "adaptive")
    lower_value, upper_value = _validate_bounds(lower, upper)

    if adaptive_value:
        return self._minimize_integer(
            objective,
            lower_value,
            upper_value,
        )
    return self._minimize_continuous(
        objective,
        lower_value,
        upper_value,
    )

auto_bounds staticmethod

auto_bounds(
    coords: ndarray,
    adaptive: bool,
    bandwidth_type: str = "gwr",
) -> Tuple[float, float]

Derive safe default bandwidth-search bounds from coordinates.

Parameters:

Name Type Description Default
coords ndarray

Finite numeric coordinates.

required
adaptive bool

Whether the bandwidth represents an integer neighbour count.

required
bandwidth_type str

Retained for API compatibility. Both values currently use the same robust bounds; unsupported values are rejected rather than ignored.

'gwr'

Returns:

Type Description
Tuple[float, float]

(lower, upper) : tuple of float: Valid ordered search bounds.

Notes

Adaptive bounds use [20, n] for datasets with at least 40 samples and a 5%-based lower bound (at least 2) for smaller datasets. Fixed bounds are based on positive observed pairwise distances, avoiding zero-width bounds for repeated or degenerate coordinates.

Source code in src/pygwrx/core/optimization.py
@staticmethod
def auto_bounds(
    coords: np.ndarray,
    adaptive: bool,
    bandwidth_type: str = "gwr",
) -> Tuple[float, float]:
    """Derive safe default bandwidth-search bounds from coordinates.

    Args:
        coords: Finite numeric coordinates.
        adaptive: Whether the bandwidth represents an integer neighbour count.
        bandwidth_type: Retained for API compatibility. Both values currently use the same
            robust bounds; unsupported values are rejected rather than ignored.

    Returns:
        (lower, upper) : tuple of float: Valid ordered search bounds.

    Notes:
        Adaptive bounds use ``[20, n]`` for datasets with at least 40 samples
        and a 5%-based lower bound (at least 2) for smaller datasets. Fixed bounds
        are based on positive
        observed pairwise distances, avoiding zero-width bounds for repeated or
        degenerate coordinates.
    """
    adaptive_value = _validate_bool(adaptive, "adaptive")

    if not isinstance(bandwidth_type, str):
        raise TypeError("bandwidth_type must be a string.")
    bandwidth_name = bandwidth_type.strip().lower()
    if bandwidth_name not in {"gwr", "bandwidth"}:
        raise ValueError(
            "bandwidth_type must be either 'gwr' or 'bandwidth'; "
            f"got {bandwidth_type!r}."
        )

    try:
        coords_arr = np.asarray(coords, dtype=float)
    except (TypeError, ValueError) as exc:
        raise TypeError("coords must contain numeric values.") from exc

    if coords_arr.ndim != 2:
        raise ValueError(
            "coords must be a two-dimensional array with shape "
            "(n_samples, n_dimensions)."
        )
    if coords_arr.shape[0] < 2:
        raise ValueError("At least two coordinate observations are required.")
    if coords_arr.shape[1] < 1:
        raise ValueError("coords must contain at least one coordinate dimension.")
    if not np.all(np.isfinite(coords_arr)):
        raise ValueError("coords must contain only finite values.")

    n_samples = coords_arr.shape[0]

    if adaptive_value:
        lower = max(2, int(np.ceil(0.05 * n_samples)))
        if n_samples >= 40:
            lower = max(20, lower)
        lower = min(lower, n_samples)
        return float(lower), float(n_samples)

    unique_coords = np.unique(coords_arr, axis=0)
    if unique_coords.shape[0] < 2:
        raise ValueError(
            "Cannot derive fixed-bandwidth bounds because all pairwise "
            "coordinate distances are zero."
        )

    # Use nearest-neighbour distances for a local lower scale without
    # materializing the O(n^2) condensed pairwise-distance vector.
    tree = cKDTree(unique_coords)
    nearest_distances, _ = tree.query(unique_coords, k=2)
    positive_nearest = nearest_distances[:, 1]
    positive_nearest = positive_nearest[positive_nearest > 0.0]

    bbox_min = unique_coords.min(axis=0)
    bbox_max = unique_coords.max(axis=0)
    upper = float(np.linalg.norm(bbox_max - bbox_min))
    lower = float(np.percentile(positive_nearest, 5.0))

    if not np.isfinite(lower) or lower <= 0.0:
        lower = float(np.min(positive_nearest))

    # With only one distinct positive distance, construct a non-degenerate
    # interval while preserving the observed distance as the upper bound.
    if lower >= upper:
        lower = upper / 1000.0
        if lower <= 0.0:
            lower = np.nextafter(0.0, 1.0)

    return lower, upper

BrentSearch

Brent's bounded method for continuous one-dimensional minimization.

Property Value
Type class
Import from pygwrx.core import BrentSearch
Signature BrentSearch(tol: 'float' = 1e-05, max_iter: 'int' = 100, verbose: 'bool' = True)
Maintained example examples/core/06_optimization.py

BrentSearch

BrentSearch(
    tol: float = 1e-05,
    max_iter: int = 100,
    verbose: bool = True,
)

Brent's bounded method for continuous one-dimensional minimization.

Parameters:

Name Type Description Default
tol float

Positive relative/absolute convergence tolerance.

1e-05
max_iter int

Maximum number of optimization updates.

100
verbose bool

Whether to print progress information.

True
Notes

Brent's method is a continuous optimizer. Adaptive integer bandwidths should normally use GoldenSectionSearch(..., adaptive=True) or an explicit integer post-processing step in the bandwidth selector.

Source code in src/pygwrx/core/optimization.py
def __init__(
    self,
    tol: float = 1e-5,
    max_iter: int = 100,
    verbose: bool = True,
):
    self.tol = _validate_positive_float(tol, "tol")
    self.max_iter = _validate_positive_int(max_iter, "max_iter")
    self.verbose = _validate_bool(verbose, "verbose")

minimize

minimize(
    func: Callable[[float], float],
    lower: float,
    upper: float,
) -> OptimizationResult

Minimize a scalar objective on the closed interval [lower, upper].

Source code in src/pygwrx/core/optimization.py
def minimize(
    self,
    func: Callable[[float], float],
    lower: float,
    upper: float,
) -> OptimizationResult:
    """Minimize a scalar objective on the closed interval ``[lower, upper]``."""
    objective = _validate_objective(func)
    lower_value, upper_value = _validate_bounds(lower, upper)
    evaluator = _ObjectiveEvaluator(objective, integer=False)

    if lower_value == upper_value:
        score = evaluator(lower_value)
        finite = np.isfinite(score)
        return OptimizationResult(
            value=lower_value,
            score=score,
            iterations=0,
            converged=bool(finite),
            evaluations=evaluator.evaluations,
            message=(
                "The search interval contains a single finite candidate."
                if finite
                else "The single search candidate has a non-finite score."
            ),
        )

    a = lower_value
    b = upper_value

    # Record endpoints so constrained boundary minima can be returned.
    fa = evaluator(a)
    fb = evaluator(b)

    x = w = v = a + self.GOLDEN * (b - a)
    fx = fw = fv = evaluator(x)

    # If the initial interior point is invalid but a finite endpoint exists,
    # probe the midpoint before deciding whether the interior is unusable.
    if not np.isfinite(fx):
        midpoint = 0.5 * (a + b)
        fm = evaluator(midpoint)
        if np.isfinite(fm):
            x = w = v = midpoint
            fx = fw = fv = fm
        elif np.isfinite(fa) or np.isfinite(fb):
            best_value, best_score = evaluator.best()
            return OptimizationResult(
                value=float(best_value),
                score=float(best_score),
                iterations=0,
                converged=True,
                evaluations=evaluator.evaluations,
                message=(
                    "Only a finite boundary candidate was found; returned the "
                    "best constrained endpoint."
                ),
            )
        else:
            return OptimizationResult(
                value=float(x),
                score=np.inf,
                iterations=0,
                converged=False,
                evaluations=evaluator.evaluations,
                message="No finite objective value was found at initialization.",
            )

    if self.verbose:
        print("  Starting Brent's Method")
        print(f"  Search interval: [{a:.6g}, {b:.6g}]")

    d = 0.0
    e = 0.0
    iterations = 0
    converged_interval = False

    while iterations < self.max_iter:
        midpoint = 0.5 * (a + b)
        tol1 = self.tol * abs(x) + self.ZEPS
        tol2 = 2.0 * tol1

        if abs(x - midpoint) <= (tol2 - 0.5 * (b - a)):
            converged_interval = True
            break

        use_parabola = (
            abs(e) > tol1
            and np.isfinite(fx)
            and np.isfinite(fw)
            and np.isfinite(fv)
        )

        if use_parabola:
            r = (x - w) * (fx - fv)
            q = (x - v) * (fx - fw)
            p = (x - v) * q - (x - w) * r
            q = 2.0 * (q - r)

            if q > 0.0:
                p = -p
            q = abs(q)
            previous_e = e
            e = d

            acceptable = (
                q > 0.0
                and abs(p) < abs(0.5 * q * previous_e)
                and p > q * (a - x)
                and p < q * (b - x)
            )

            if acceptable:
                d = p / q
                u = x + d
                if (u - a) < tol2 or (b - u) < tol2:
                    d = tol1 if midpoint >= x else -tol1
            else:
                e = a - x if x >= midpoint else b - x
                d = self.GOLDEN * e
        else:
            e = a - x if x >= midpoint else b - x
            d = self.GOLDEN * e

        step = d if abs(d) >= tol1 else (tol1 if d >= 0.0 else -tol1)
        u = x + step
        fu = evaluator(u)
        iterations += 1

        if fu <= fx:
            if u >= x:
                a = x
            else:
                b = x
            v, w, x = w, x, u
            fv, fw, fx = fw, fx, fu
        else:
            if u < x:
                a = u
            else:
                b = u
            if fu <= fw or w == x:
                v, w = w, u
                fv, fw = fw, fu
            elif fu <= fv or v == x or v == w:
                v = u
                fv = fu

        if self.verbose and iterations % 5 == 0:
            best_value, best_score = evaluator.best()
            print(
                f"  Iteration {iterations}: x={float(best_value):.6g}, "
                f"f(x)={best_score:.6g}, interval=[{a:.6g}, {b:.6g}]"
            )

    best_value, best_score = evaluator.best()
    finite_solution = np.isfinite(best_score)
    converged = bool(converged_interval and finite_solution)

    if converged:
        message = "Converged because the bounded Brent criterion was satisfied."
    elif not finite_solution:
        message = "No finite objective value was found in the search interval."
    else:
        message = "Maximum iterations reached before Brent convergence."

    if self.verbose:
        print(f"  Converged: {converged}")
        print(f"  Final interval width: {b - a:.6g}")
        print(f"  Optimal value: {float(best_value):.6g}")
        print(f"  Optimal score: {best_score:.6g}")
        print(f"  Objective evaluations: {evaluator.evaluations}")

    return OptimizationResult(
        value=float(best_value),
        score=float(best_score),
        iterations=iterations,
        converged=converged,
        evaluations=evaluator.evaluations,
        message=message,
    )

Runnable examples used on this page

examples/core/06_optimization.py
# SPDX-FileCopyrightText: 2026 Jinghao Hu
# SPDX-License-Identifier: MIT

"""Use both public scalar optimizers and the OptimizationResult container."""

# Allow this script to run directly from any working directory.
import sys
from pathlib import Path

_PROJECT_ROOT = Path(__file__).resolve().parents[2]
_EXAMPLES_ROOT = _PROJECT_ROOT / "examples"
_SRC_ROOT = _PROJECT_ROOT / "src"
for _path in (_SRC_ROOT, _EXAMPLES_ROOT):
    if str(_path) not in sys.path:
        sys.path.insert(0, str(_path))

from pygwrx.core import BrentSearch, GoldenSectionSearch, OptimizationResult


def objective(x):
    """Simple convex objective with a known minimum."""
    return (x - 2.5) ** 2 + 1.0


golden = GoldenSectionSearch(tol=1e-7, max_iter=100, verbose=False)
brent = BrentSearch(tol=1e-7, max_iter=100, verbose=False)
print("golden=", golden.minimize(objective, 0.0, 5.0))
print("brent=", brent.minimize(objective, 0.0, 5.0))
print("manual_result=", OptimizationResult(2.5, 1.0, 10, True, evaluations=12))