diff --git a/CHANGELOG.md b/CHANGELOG.md index a8688eb..4b6e661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 interleaves the endpoints of unrelated edges. - `IntegrateFunction` names itself in its errors; the adaptive quadrature no longer reports under a name absent from the surface. +- `MinimiseDifferentialEvolution` refuses a non-finite bound instead + of drawing a NaN population and returning a NaN point with no + error. ## [1.0.0] - 2026-09-03 diff --git a/optim/devolution.go b/optim/devolution.go index 945fdce..28ecf22 100644 --- a/optim/devolution.go +++ b/optim/devolution.go @@ -58,8 +58,12 @@ func MinimiseDifferentialEvolution(f func(*core.Array) (float64, error), return nil, 0, base.Errf("%s: complex bounds are not supported", name) } for i := range n { - if !(upper.FloatAt(i) > lower.FloatAt(i)) { - return nil, 0, base.Errf("%s: bound %d runs from %g to %g", name, i, lower.FloatAt(i), upper.FloatAt(i)) + lo, up := lower.FloatAt(i), upper.FloatAt(i) + // An infinite side has no uniform draw: the population would be + // born NaN and the best point would come back NaN with no error, + // so a non-finite bound is degenerate exactly as a crossed one is. + if math.IsNaN(lo) || math.IsNaN(up) || math.IsInf(lo, 0) || math.IsInf(up, 0) || !(up > lo) { + return nil, 0, base.Errf("%s: bound %d runs from %g to %g", name, i, lo, up) } } pop := opts.Population diff --git a/optim/devolution_test.go b/optim/devolution_test.go index a9acb92..0cbb9d3 100644 --- a/optim/devolution_test.go +++ b/optim/devolution_test.go @@ -112,3 +112,25 @@ func TestDEErrors(t *testing.T) { t.Error("NaN objective accepted") } } + +// TestDERejectsInfiniteBounds pins that an infinite side is refused: the +// sampler draws uniformly inside the box, so an open side would populate +// it with NaN and hand back a NaN point with a nil error. +func TestDERejectsInfiniteBounds(t *testing.T) { + zero, _ := core.FromFloats([]float64{0}, 1) + one, _ := core.FromFloats([]float64{1}, 1) + constF := func(a *core.Array) (float64, error) { return 1, nil } + for _, tc := range []struct { + name string + lo, hi *core.Array + }{ + {"both sides open", mustBound(math.Inf(-1)), mustBound(math.Inf(1))}, + {"upper open", zero, mustBound(math.Inf(1))}, + {"lower open", mustBound(math.Inf(-1)), one}, + } { + x, _, err := MinimiseDifferentialEvolution(constF, tc.lo, tc.hi, DifferentialEvolutionOptions{Generations: 5}) + if err == nil { + t.Errorf("%s: infinite bound accepted, returned x=%g", tc.name, x.FloatAt(0)) + } + } +}