lanl / lanl/PyBNF

PSO collision tie-break can loop forever: a fixed 1e-6 perturbation is below one ULP for a linear parameter above ~1e10

Open
#721 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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:

  1. The perturbation is re-derived from self.swarm[p][0] each iteration, not from the previous new_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.
  2. 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

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.

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.