fix(optim): refuse non-finite bounds in differential evolution
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
interleaves the endpoints of unrelated edges.
|
interleaves the endpoints of unrelated edges.
|
||||||
- `IntegrateFunction` names itself in its errors; the adaptive
|
- `IntegrateFunction` names itself in its errors; the adaptive
|
||||||
quadrature no longer reports under a name absent from the surface.
|
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
|
## [1.0.0] - 2026-09-03
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -58,8 +58,12 @@ func MinimiseDifferentialEvolution(f func(*core.Array) (float64, error),
|
|||||||
return nil, 0, base.Errf("%s: complex bounds are not supported", name)
|
return nil, 0, base.Errf("%s: complex bounds are not supported", name)
|
||||||
}
|
}
|
||||||
for i := range n {
|
for i := range n {
|
||||||
if !(upper.FloatAt(i) > lower.FloatAt(i)) {
|
lo, up := lower.FloatAt(i), upper.FloatAt(i)
|
||||||
return nil, 0, base.Errf("%s: bound %d runs from %g to %g", name, i, 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
|
pop := opts.Population
|
||||||
|
|||||||
@@ -112,3 +112,25 @@ func TestDEErrors(t *testing.T) {
|
|||||||
t.Error("NaN objective accepted")
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user