PSO collision tie-break can loop forever: a fixed 1e-6 perturbation is below one ULP for a linear parameter above ~1e10
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 25
- Forks
- 25
- Avg merge
- 2h 6m
- Merged PRs (30d)
- 95
Description
What happens
ParticleSwarmOptimization.got_result breaks a duplicate-position collision by perturbing the particle's parameters by ±1e-6 until the result is no longer in pset_map (pybnf/algorithms/optimizers/particle_swarm.py:258-267):
new_pset = PSet(new_vars)
self.swarm[p][0] = new_pset
# This will cause a crash if new_pset happens to be the same as an already running pset in pset_map.
# This could come up in practice if all parameters have hit a box constraint.
# As a simple workaround, perturb the parameters slightly
while new_pset in self.pset_map:
new_pset = PSet([v.add_rand(-1e-6, 1e-6, self.rng) for v in self.swarm[p][0]])
For a linear (non-log) parameter of magnitude above roughly 1e10, a ±1e-6 perturbation is smaller than one ULP of the value, so add_rand returns the value unchanged. PSet equality and hashing are by value, so the "perturbed" set compares equal to the original, the while condition stays true, and the loop never terminates. The fit hangs — no exception, no log line, no progress.
Two details make it a hard loop rather than a slow one:
- The perturbation is re-derived from
self.swarm[p][0]each iteration, not from the previousnew_pset.self.swarm[p][0]is never reassigned inside the loop, so every iteration recomputes from the identical base. There is no random walk that could eventually accumulate past one ULP. - There is no iteration cap and no fallback.
For normal-magnitude parameters the loop terminates immediately because the draws land on distinct representable values, which is why this has not been seen. The failure needs a large-magnitude linear parameter and a collision — and the comment above the loop names exactly when collisions happen ("if all parameters have hit a box constraint"), which is also when a parameter is likeliest to be sitting on a large bound.
Reproduction
The arithmetic, verified in the repo:
import numpy as np
from pybnf.pset import FreeParameter, PSet
rng = np.random.default_rng(0)
print('ULP(1e12) =', np.spacing(1e12))
p = FreeParameter('x__FREE', 'uniform_var', 0, 1e12, 1e12)
q = p.add_rand(-1e-6, 1e-6, rng)
print('orig =', p.value, ' perturbed =', q.value, ' changed?', q.value != p.value)
a, b = PSet([p]), PSet([q])
print('PSets equal?', a == b, '| same hash?', hash(a) == hash(b))
Observed:
ULP(1e12) = 0.0001220703125
orig = 1000000000000.0 perturbed = 1000000000000.0 changed? False
PSets equal? True | same hash? True
Since a == b and the loop's exit test is new_pset in self.pset_map, the condition is permanently true. ULP(1e12) is 1.2e-4, roughly 100× the perturbation magnitude; the no-op threshold is around 1e10 (ULP ≈ 2e-6).
To trigger it end to end: a PSO job with a uniform_var whose bounds reach ~1e12, run until two particles land on the same position — the scenario the comment at line 260 anticipates.
Suggested shape
Not applied — this was found in a read-only review. The robust fix is to scale the perturbation to the value rather than using a fixed absolute epsilon (e.g. np.spacing(v.value) or a relative factor), and to bound the loop with a fallback so a pathological case degrades instead of hanging.
Context
Found while adjudicating a different, unrelated claim about this same block (that the perturbed set is never written back to self.swarm[p][0]). That claim was investigated and refuted — the missing write-back costs a repeated 1e-6 tie-break each iteration but never mis-attributes a score, because every recorded (pset, score) pair uses res.pset, which is the point actually simulated. This loop-termination defect is a separate, genuine consequence of the same code.
Where
pybnf/algorithms/optimizers/particle_swarm.py:264.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in pybnf/algorithms/optimizers/particle_swarm.py at ParticleSwarmOptimization.got_result, especially lines 258-267, and review the supplied large-magnitude reproduction. Verify that a collision involving a parameter near 1e12 no longer hangs and that the tie-break still produces a usable distinct position for ordinary values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- numpy, python
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100