challenges / morse-clusters-rho14
Morse clusters at rho = 14, minimum energy
# Morse clusters at rho = 14, minimum energy
## Goal
`cluster.py` exposes `cluster(n: int, time_budget: float, seed: int) -> list[tuple[float, float, float]]`:
`n` atoms `(x, y, z)` anywhere in 3-D space. Minimise the Morse energy
E = sum over i < j of x_ij (x_ij - 2), x_ij = exp(rho (1 - r_ij)), rho = 14, r_ij = |x_i - x_j|
in reduced units (pair well depth 1, equilibrium pair separation 1). The range parameter `rho`
sets how short-ranged the potential is; `rho = 14` is the hardest column of the Cambridge Cluster
Database's Morse table (`morse-clusters-rho6` is the Lennard-Jones-like sibling). With such a narrow
well, strain is expensive and nearest-neighbour count is nearly everything: the global minima are
close-packed (fcc and hcp) or decahedral fragments rather than icosahedra, the energy landscape is
far rougher, and the number of local minima explodes. Thirteen values of `n` between 20 and 80 are
the benchmark. The database authors describe locating every Morse global minimum up to `n = 80`
as "a significant achievement and one which no unbiased global optimization algorithm has yet
managed", and the short-ranged clusters are the ones that defeat most methods.
## Metric
metric = mean over n in NS of E(n) / record(n)
Both energies are negative, so 1.0 means matching every record and above 1.0 means beating at
least one (a lower, more negative energy gives a larger ratio). A cluster with `E >= 0` scores 0
for that `n`. The eval recomputes the energy itself from the returned coordinates; a wrong count, a
non-finite coordinate, two coincident atoms (`r < 1e-9`) or a non-finite energy is a failed run.
Nothing your solver reports is used. `ZT_EVAL_SEED` only changes the `seed` handed to `cluster`,
so your method must be robust to its starting point.
## Records
Best-known energies at `rho = 14` from the Cambridge Cluster Database "Morse Clusters: Table of
Global Minima" (J. P. K. Doye, D. J. Wales and R. S. Berry, J. Chem. Phys. 103, 4234 (1995);
J. P. K. Doye and D. J. Wales, J. Chem. Soc. Faraday Trans. 93, 4233 (1997), with later additions
credited on the page; https://www-wales.ch.cam.ac.uk/~wales/CCD/jon/structures/Morse/tables.html,
fetched 2026-09-07), exactly as printed there (6 decimals; the entry in bold in the `rho = 14`
column is the global minimum for that `n`). Every entry was recomputed from the database's own
points file: the files store the geometry at the `rho` given on their first line (6 or 10 for these
entries, 14 for 30D'), so each was relaxed locally at `rho = 14` (ramping `rho` up in steps of 1,
then L-BFGS to a gradient below 1e-5), and every relaxed energy agrees with the printed value to
within 5e-7. None is proven optimal. An energy below a record by more than 1e-5 is listed in
`records_beaten`.
| n | record E | database label | point group | nearest-neighbour pairs | rho of points file |
|---|---|---|---|---|---|
| 20 | -64.791953 | 20B | C2v | 65 | 6 |
| 23 | -77.302495 | 23C | Cs | 78 | 10 |
| 30 | -106.835790 | 30D' | Cs | 107 | 14 |
| 38 | -144.321054 | 38D | Oh | 144 | 6 |
| 45 | -174.511633 | 45E | C2v | 175 | 6 |
| 50 | -198.455633 | 50D | D3h | 198 | 6 |
| 55 | -220.646208 | 55C | C2v | 221 | 6 |
| 59 | -240.572493 | 59E | Td | 240 | 6 |
| 64 | -264.587042 | 64C | C2v | 265 | 6 |
| 68 | -282.683003 | 68H | C3v | 282 | 6 |
| 72 | -302.413229 | 72E | C1 | 303 | 6 |
| 75 | -318.407330 | 75C | D5h | 319 | 6 |
| 80 | -340.811371 | 80G | Cs | 340 | 6 |
Structural notes from the table: `n = 38` is the fcc truncated octahedron (38D, Oh), `n = 50` a
D3h close-packed fragment, `n = 59` a Td fragment, `n = 75` the Marks decahedron (75C, D5h),
`n = 80` a Cs close-packed structure; the low-symmetry entries (`n = 72`, C1) are close-packed with
stacking faults. 30D' is a later addition to the page, not credited there by name (the intro
credits the other primed entries to Branz, to Leary and Chan, and to Locatelli and Schoen). The
`rho = 6` file of 32E does not relax into the printed `rho = 14` minimum, so `n = 32` is excluded.
## Constraints
- Standard library only. No numpy, no scipy. The eval rejects other imports.
- Respect `time_budget` (seconds, per call). The eval fails a call that runs past 1.25x + 3 s.
- Deterministic given `seed`: use `random.Random(seed)`.
## Iterating
- `ZT_EVAL_NS=38,75` runs a subset of n; `ZT_EVAL_PER_N_SECONDS=2` shortens the per-n budget. The
default is all thirteen n at 7 s each (about 95 s). Run `python eval.py` in your workspace.
- The per-n detail in the eval output shows which n are furthest from their record.
## Ideas that are known to matter (check the journal before repeating one)
- One descent from a lattice cut lands a few per cent above the record. Basin hopping (perturb,
locally minimise, Metropolis accept) with a properly converged local minimiser is what found
these entries; the database authors used exactly that, and at `rho = 14` they needed seeding
from structural families, not just random kicks.
- Structure follows the range: at `rho = 14` strain is prohibitive, so the winners are fcc and hcp
fragments (truncated octahedra, twinned close-packed pieces with stacking faults) and Marks
decahedra. Enumerate close-packed fragments by nearest-neighbour count (`nn` in the table), then
relax; icosahedral starts are usually wrong here.
- Surface moves beat random kicks: remove the atom with the lowest number of contacts and re-place
it at the site with the most, then re-minimise. With a well this narrow, moves that keep the
contact count are the only ones that survive.
- The ratio only rounds to 1.0 once you are within about 1e-8 relative, so finish each minimum
with a well-converged local optimiser (conjugate gradient or L-BFGS). The Hessian is very stiff
at `rho = 14`: steepest descent needs a tiny step, so a quasi-Newton polish pays off much more
than at `rho = 6`.
- The pure-Python pair loop is the bottleneck: `exp` is the expensive call, so cache `x` per pair
and compute energy and forces in one pass. Pairs beyond `r = 2` contribute less than 1e-6 each
at `rho = 14` and can be skipped during minimisation, but keep the full sum for the final energy.
Write one honest line in `NOTES.md`: the idea, and which `n` it helped.
Simpler is better: all else equal prefer the shorter solver, and treat removing code for an
equal score as a win. Log every experiment, including discards, in your results.tsv.