StructureOptimizer#
- class pasted._optimizer.StructureOptimizer(*, n_atoms: int, charge: int, mult: int, objective: dict[str, float] | Callable[[dict[str, float]], float] | Callable[[dict[str, float], EvalContext], float], elements: str | list[str] | None = None, method: str = 'annealing', max_steps: int = 5000, T_start: float = 1.0, T_end: float = 0.01, frag_threshold: float = 0.3, move_step: float = 0.5, allow_composition_moves: bool = True, allow_displacements: bool = True, allow_affine_moves: bool = False, affine_strength: float = 0.1, affine_stretch: float | None = None, affine_shear: float | None = None, affine_jitter: float | None = None, lcc_threshold: float = 0.0, cov_scale: float = 1.0, relax_cycles: int = 1500, cutoff: float | None = None, n_bins: int = 20, w_atom: float = 0.5, w_spatial: float = 0.5, n_restarts: int = 1, n_replicas: int = 4, pt_swap_interval: int = 10, max_init_attempts: int = 0, seed: int | None = None, verbose: bool = False)[source]#
Bases:
objectOptimize a single structure to maximize a disorder objective.
- Parameters:
n_atoms – Number of atoms.
charge – Total system charge.
mult – Spin multiplicity 2S+1.
objective –
Weight dict
{"METRIC": weight, ...}or any callable. The optimizer maximizes the returned scalar.Two calling conventions are supported:
1-argument
f(m)—mis adict[str, float]of disorder metrics. Fully backward-compatible.2-argument
f(m, ctx)—mis the same metrics dict;ctxis anEvalContextthat exposes:Structure:
ctx.atoms,ctx.positions,ctx.charge,ctx.mult,ctx.n_atoms,ctx.to_xyz()Optimizer state:
ctx.step,ctx.temperature,ctx.f_current,ctx.best_f,ctx.progress,ctx.per_atom_q6,ctx.restart_idxConfiguration:
ctx.element_pool,ctx.cutoff,ctx.method,ctx.T_start,ctx.T_end,ctx.seedPT-only (
Nonefor other methods):ctx.replica_idx,ctx.replica_temperature,ctx.n_replicas
Dispatch is based on the number of required positional parameters via
inspect.signature(). A callable with a default for the second argument (lambda m, ctx=None:) is treated as 1-argument.EvalContextconstruction is skipped entirely for 1-argument and dict objectives — no overhead for existing code.elements – Element pool — spec string (
"6,7,8"), list of symbols, orNonefor all Z = 1–106. When a list is given, duplicate symbols are silently removed while preserving insertion order (e.g.['C', 'H', 'H', 'H', 'H']is treated as['C', 'H']). To bias sampling toward a particular element useelement_fractionsinStructureGeneratorinstead.method –
"annealing"(default),"basin_hopping", or"parallel_tempering".max_steps – Number of MC steps per restart (or per replica per restart for
"parallel_tempering"; default: 5000).T_start – Initial temperature (default: 1.0). For
"parallel_tempering"this is the highest replica temperature.T_end – Final temperature for SA (default: 0.01). For
"parallel_tempering"this is the lowest replica temperature (the coldest, most selective replica). BH uses T_start throughout.n_replicas – Number of temperature replicas for
"parallel_tempering"(default: 4). Ignored for other methods. Temperatures are spaced geometrically between T_end and T_start.pt_swap_interval – Attempt a replica-exchange swap every this many MC steps (default: 10). Ignored for other methods.
allow_displacements – When
True(default), fragment moves — small random displacements of one or more atoms — are included in the MC step pool as an independent move type. WhenFalse, fragment moves are excluded; coordinates are only modified by affine moves (if allow_affine_moves isTrue). If the initial structure passed torun()contains atoms whose symbols are not in elements, those atoms are automatically replaced with parity-compatible pool elements before the MC loop begins. This sanitization applies to all three methods (SA, BH, and PT); see_sanitize_atoms_to_pool(). Cannot beFalsesimultaneously with allow_composition_moves and allow_affine_moves (at least one move type must be enabled).allow_composition_moves – When
True(default), composition moves — replacing a random atom with a different element drawn from elements while preserving charge/multiplicity parity — are included in the MC step pool as an independent move type. WhenFalse, element types are held fixed throughout the run. Cannot beFalsesimultaneously with allow_displacements and allow_affine_moves (at least one move type must be enabled).allow_affine_moves – When
True, affine moves — a random stretch, compress, or shear applied to the entire structure, followed by a small per-atom jitter — are included in the MC step pool as an independent move type alongside (not as a subset of) displacement and composition moves. Affine moves allow the optimizer to explore elongated or compressed configurations that fragment moves cannot reach efficiently. When allow_displacements isFalse, affine moves are the only way positions change; the distance-constraint relaxation is not applied after affine moves (consistent with the allow_displacements=False semantics). Default:False(backward-compatible). Cannot beFalsesimultaneously with allow_displacements and allow_composition_moves (at least one move type must be enabled).affine_strength – Global dimensionless scale of the affine transform (default: 0.1). At 0.1 the structure is stretched / compressed by up to ±10 % along a random axis and sheared by up to ±5 %. Practical range: 0.02–0.4. Has no effect when allow_affine_moves is
False. Use affine_stretch, affine_shear, and affine_jitter to override individual operation strengths independently.affine_stretch – Strength of the stretch/compress operation only ∈ (0, 1). When
None(default) affine_strength is used. Set to0.0to disable stretching while keeping shear and jitter active. Has no effect when allow_affine_moves isFalse.affine_shear – Strength of the shear operation only ∈ (0, 1). When
None(default) affine_strength is used. Set to0.0to disable shearing while keeping stretch and jitter active. Has no effect when allow_affine_moves isFalse.affine_jitter – Per-atom jitter scale ∈ (0, 1) relative to move_step. When
None(default) affine_strength is used. Set to0.0to disable per-atom jitter in affine moves. Has no effect when allow_affine_moves isFalse.frag_threshold – Local Q6 threshold for fragment selection (default: 0.3). Atoms with local Q6 > threshold are preferentially displaced.
move_step – Maximum displacement magnitude per coordinate step (Å, default: 0.5). Also used as the per-atom jitter scale in affine moves (× 0.25).
lcc_threshold – Minimum
graph_lccrequired to accept a step (default: 0.0, i.e. no connectivity constraint). Set to 0.8 to enforce that at least 80 % of atoms remain connected.cov_scale – Minimum distance scale factor for
relax_positions().relax_cycles – Max repulsion-relaxation cycles per step. Basin-Hopping uses 3× this value for its local-minimisation step.
cutoff – Distance cutoff (Å) for Steinhardt / graph metrics. Auto-computed from the element pool when
None.n_bins – Histogram bins for
H_spatial/RDF_dev(default: 20).w_atom – Weight of
H_atominH_total(default: 0.5).w_spatial – Weight of
H_spatialinH_total(default: 0.5).n_restarts – Independent optimization runs (default: 1). The best result across all restarts is returned.
max_init_attempts –
Maximum number of single-sample tries that
_make_initial()makes per restart when generating the starting structure (default:0= unlimited).0— unlimited retries (recommended for production runs with large or constrained element pools). Safe because__init__()validates at construction time that the element pool can satisfy the charge/multiplicity parity constraint; if that check passes, a valid structure is guaranteed to be found eventually.> 0— at most max_init_attempts tries per restart. If exhausted the restart is skipped and aUserWarningis emitted. Useful as a time-budget guard in automated pipelines.
Note
__init__()raisesValueErrorimmediately when the element pool is structurally incompatible withcharge/mult(e.g. an all-nitrogen pool withcharge=0, mult=1), making an infinite loop impossible for well-formed inputs.seed – Random seed (
None→ non-deterministic).verbose – Print per-step progress to stderr (default:
False).
Examples
Class API:
from pasted import StructureOptimizer opt = StructureOptimizer( n_atoms=50, charge=0, mult=1, elements="24,25,26,27,28", # Cantor alloy objective={"H_atom": 1.0, "H_spatial": 1.0, "Q6": -2.0}, method="annealing", max_steps=5000, lcc_threshold=0.8, seed=42, ) best = opt.run()
Callable objective:
opt = StructureOptimizer( ..., objective=lambda m: m["H_spatial"] - 2.0 * m["Q6"], )
- run(initial: Structure | None = None) OptimizationResult[source]#
Run
n_restartsoptimizations and return anOptimizationResult.Each restart begins from an independently generated random gas-mode structure (or from initial if provided). All per-restart results are collected, sorted by objective value (highest first), and returned together in an
OptimizationResult.OptimizationResultis list-compatible:result[0]andresult.bestboth return the highest-scoring structure, andfor s in resultiterates all restarts in rank order. Existing code that callsopt.run()and uses the return value as a singleStructureshould switch toopt.run().bestoropt.run()[0].A
UserWarningis emitted when one or more restarts fail to produce a valid initial structure after all internal retries are exhausted. Transient parity-check failures inside the initial- structure generation loop are silenced internally and do not reach the caller; only a definitive inability to start a restart is reported. The retry limit is controlled bymax_init_attempts(0= unlimited, the default).- Parameters:
initial – Starting structure. When
None(default), a random gas-mode structure is generated automatically for each restart.- Returns:
All per-restart structures sorted by objective value (highest first), plus summary metadata. Raises
RuntimeErrorif every restart fails to produce a valid initial structure.- Return type:
OptimizationResult
- Raises:
RuntimeError – When all restarts fail to produce a valid initial structure.
Examples
Best structure only:
result = opt.run() print(result.best) # highest-scoring structure print(result[0]) # same — index 0 is always the best print(result.summary()) # one-line diagnostic
All restarts:
result = opt.run() for rank, s in enumerate(result, 1): print(f"rank {rank}: f={result.objective_scores[rank-1]:.4f} {s}")
- pasted._optimizer.parse_objective_spec(specs: list[str]) dict[str, float][source]#
Parse
["METRIC:WEIGHT", ...]into a weight dict.- Parameters:
specs – Each string must be of the form
"METRIC:WEIGHT", e.g.["H_atom:1.0", "Q6:-2.0"].- Return type:
- Raises:
ValueError – On malformed strings or unknown metric names.
Note
Parity validation at construction time.
StructureOptimizer checks at __init__ time that the element
pool can produce at least one composition of n_atoms atoms that
satisfies the charge/multiplicity parity constraint. If it cannot,
ValueError is raised immediately — before any call to
run(). This makes
max_init_attempts=0 (unlimited retries) safe: if construction
succeeds, a valid initial structure is guaranteed to eventually be found.
Note
Move-type constraints.
allow_displacements=False and allow_composition_moves=False cannot
both be set at the same time unless allow_affine_moves=True. Setting
all three to False raises ValueError.
Affine moves
When allow_affine_moves=True, half of all displacement moves are
replaced by random affine transforms (stretch / compress along one axis,
shear one axis pair, and per-atom jitter). This lets the optimizer
explore anisotropic configurations that fragment moves cannot reach
efficiently.
Unlike in StructureGenerator, the
affine_jitter term does have a visible effect here because
move_step is non-zero during MC steps.
Position-only optimization
Set allow_composition_moves=False to fix the stoichiometry and only
move atoms:
result = opt.run(initial=my_structure)
assert sorted(result.best.atoms) == sorted(my_structure.atoms)
Composition-only optimization
Set allow_displacements=False to fix the atomic coordinates and
only swap element labels. Atoms outside the pool are automatically
replaced by parity-compatible pool elements before the first MC step,
so cross-pool starting structures work with all three methods:
result = opt.run(initial=my_structure)
import numpy as np
np.testing.assert_allclose(
np.array(result.best.positions), np.array(my_structure.positions)
)
OptimizationResult#
- class pasted._optimizer.OptimizationResult(all_structures: list[Structure] = <factory>, objective_scores: list[float] = <factory>, n_restarts_attempted: int = 0, method: str = 'annealing')[source]
Bases:
objectReturn value of
StructureOptimizer.run().Wraps all per-restart results and exposes the best structure as a first-class attribute. Behaves like a
list[Structure]— indexing, iteration,len, andboolall work — so callers that only want the best result can access it without changing existing code:result = opt.run() best = result.best # highest-scoring Structure best = result[0] # same — index 0 is always the best for s in result: # iterate all restarts, best first print(s.metrics["H_total"])
- all_structures
All structures produced by each restart, sorted by objective value (highest first).
all_structures[0]is always the best.- Type:
list[pasted._generator.Structure]
- objective_scores
Scalar objective values corresponding to each entry in
all_structures.
- n_restarts_attempted
Number of restarts that were actually run (may be less than
n_restartswhen initial-structure generation fails).- Type:
- method
The optimization method used (
"annealing","basin_hopping", or"parallel_tempering").- Type:
Notes
Parallel Tempering result count. For
method="parallel_tempering", each restart contributes one entry for the global best plus one entry for each replica whose final objective value differs from the global best. The totallen(result)therefore satisfies:n_restarts <= len(result) <= n_restarts * (n_replicas + 1)
For
method="annealing"andmethod="basin_hopping", each restart contributes exactly one entry, solen(result) == n_restarts_attempted.Examples
Single-structure usage (backward-compatible):
result = opt.run() result.best.to_xyz() # best structure result[0].to_xyz() # same
All-restarts usage:
result = opt.run() print(result.summary()) for rank, s in enumerate(result, 1): print(f"rank {rank}: H_total={s.metrics['H_total']:.3f}")
- all_structures: list[Structure]
- property best: Structure
The structure with the highest objective value.
- method: str = 'annealing'
- n_restarts_attempted: int = 0
EvalContext#
- class pasted._optimizer.EvalContext(atoms: tuple[str, ...], positions: tuple[tuple[float, float, float], ...], charge: int, mult: int, n_atoms: int, metrics: dict[str, float], step: int, max_steps: int, temperature: float, f_current: float, best_f: float, restart_idx: int, n_restarts: int, per_atom_q6: ndarray, replica_idx: int | None, replica_temperature: float | None, n_replicas: int | None, element_pool: tuple[str, ...], cutoff: float, method: str, T_start: float, T_end: float, seed: int | None)[source]#
Bases:
objectFull evaluation context passed as the second argument to a 2-parameter objective callable.
EvalContextconsolidates every piece of information available at the moment the objective function is called: the current structure (atoms, positions, charge/mult), all pre-computed disorder metrics, and the live optimizer state (step number, temperature, best score seen so far, etc.). This design allows user-supplied objective functions to call external quantum-chemistry or machine-learning potential tools without depending on PASTED internals, and to implement adaptive or state-aware objectives.Attributes — Structure#
- atoms:
Element symbols for the current candidate structure, one per atom (e.g.
("C", "H", "O", ...)).- positions:
Cartesian coordinates in Å, one
(x, y, z)tuple per atom.- charge:
Total system charge.
- mult:
Spin multiplicity 2S+1.
- n_atoms:
Number of atoms (
len(atoms)).- metrics:
Computed disorder metrics dict — same reference as the
margument in the objective callable. Treat as read-only.
Attributes — Optimizer Runtime State#
- step:
Current MC step index, 0-based. Ranges from 0 to
max_steps - 1. Useful for progress-dependent or curriculum objectives.- max_steps:
Total number of MC steps per restart.
- temperature:
Current temperature at this step. For
"annealing"this decreases exponentially; for"basin_hopping"it is fixed atT_start; for"parallel_tempering"it is this replica’s fixed temperature.- f_current:
Objective value of the most recently accepted state. Use this to compute improvement margins or relative scores.
- best_f:
Best objective value seen across all steps so far in this restart.
- restart_idx:
0-based index of the current restart.
- n_restarts:
Total number of restarts configured.
- per_atom_q6:
Per-atom Steinhardt Q6 values from the previous accepted step (shape
[n_atoms], dtypefloat64). Already computed by the optimizer loop; available at zero additional cost. Treat the array as read-only — it is a reference, not a copy.
Attributes — Parallel Tempering (
Nonefor other methods)#- replica_idx:
0-based index of the current replica (0 = coldest,
n_replicas - 1= hottest).Nonewhenmethod != "parallel_tempering".- replica_temperature:
This replica’s fixed temperature.
Nonewhenmethod != "parallel_tempering".- n_replicas:
Total number of replicas.
Nonewhenmethod != "parallel_tempering".
Attributes — Optimizer Configuration#
- element_pool:
Tuple of element symbols available for composition moves.
- cutoff:
Distance cutoff in Å used for Steinhardt and graph metrics.
- method:
Optimization method:
"annealing","basin_hopping", or"parallel_tempering".- T_start:
Starting temperature.
- T_end:
Ending temperature (for
"annealing").- seed:
Random seed, or
Noneif unseeded.
Full evaluation context passed as the second argument to a 2-parameter objective callable. Consolidates the current candidate structure, all pre-computed disorder metrics, and the live optimizer runtime state.
Calling conventions
Two calling conventions are supported for the
objectiveparameter ofStructureOptimizer:1-argument
f(m)—mis adict[str, float]of disorder metrics. Fully backward-compatible with all existing code.2-argument
f(m, ctx)—mis the same metrics dict;ctxis anEvalContext. Dispatch is based on the number of required positional parameters viainspect.signature(). A callable with a default for the second argument (lambda m, ctx=None:) is treated as 1-argument.
ObjectiveType alias
ObjectiveType = ( dict[str, float] | Callable[[dict[str, float]], float] | Callable[[dict[str, float], EvalContext], float] )
EvalContextis exported from the top-levelpastednamespace:from pasted import EvalContext
Example — adaptive curriculum objective
def curriculum_objective(m: dict, ctx: EvalContext) -> float: """Broad exploration early, strong Q6 penalty late.""" base = m["H_total"] if ctx.progress < 0.5: return base else: return base - 3.0 * m["Q6"] opt = StructureOptimizer( n_atoms=15, charge=0, mult=1, elements="6,7,8,16", objective=curriculum_objective, method="annealing", max_steps=4000, seed=7, )
Example — per-atom Q6 locality penalty
import numpy as np def local_disorder_objective(m: dict, ctx: EvalContext) -> float: q6_var = float(np.var(ctx.per_atom_q6)) q6_max = float(np.max(ctx.per_atom_q6)) return m["H_total"] + q6_var * 0.5 - q6_max * 1.0
- __repr__()#
Return repr(self).
- property progress: float#
step / max_steps.Returns a float in
[0.0, 1.0)useful for curriculum-style objectives that change behavior over the course of a run.- Type:
Fractional progress of the current restart
- to_xyz(comment: str = '') str[source]#
Return a well-formed XYZ-format string for the current structure.
The string is suitable for writing directly to a
.xyzfile and passing to external tools such as xTB, ORCA, or any ASE calculator.- Parameters:
comment – Optional comment placed on the second line of the XYZ block. When empty, a default comment containing charge and multiplicity is generated automatically.
- Returns:
Multi-line XYZ string (no trailing newline).
- Return type: