Feature: Add fix symmetry constraint#438
Conversation
orionarcher
left a comment
There was a problem hiding this comment.
Broadly this seems like a very nice and well-architected addition to the constraints API. Very nice work @danielzuegner. I'd tag in @thomasloux who initially implemented constraints but to me this looks good pretty much as is.
torch_sim/constraints.py
Outdated
| # Merge each group using the constraint's merge method | ||
| result = [] | ||
| for constraint_type, (constraints, state_indices) in constraints_by_type.items(): | ||
| merged = constraint_type.merge(constraints, state_indices, atom_offsets) |
There was a problem hiding this comment.
merge is a nice addition to the contraint API
…atcher When InFlightAutoBatcher concatenates states, it calls merge_constraints() which passes state_indices (the position of each source state in the concatenation list) to FixSymmetry.merge(). The old code used these state_indices as offsets: system_indices.append(offset + i). This breaks when a constraint covers multiple systems. E.g. merging constraint_A (5 systems) with constraint_B (3 systems) using state_indices=[0, 1] produces system_indices=[0,1,2,3,4, 1,2,3] — duplicates at 1,2,3 — triggering "Duplicate system indices found in SystemConstraint". Fix: use a cumulative running offset instead of state_indices, so the merged indices are always [0,1,2,...,N-1]. Co-authored-by: Cursor <cursoragent@cursor.com>
|
thanks a lot for this contribution @danielzuegner. super useful! just took it for a spin and encountered a bug:
Example: merging constraint_A (5 systems) with constraint_B (3 systems) using
- for constraint, offset in zip(constraints, state_indices, strict=False):
- for i in range(len(constraint.rotations)):
- system_indices.append(offset + i)
+ cumulative_offset = 0
+ for constraint in constraints:
+ for idx in range(len(constraint.rotations)):
+ system_indices.append(cumulative_offset + idx)
+ cumulative_offset += len(constraint.rotations)i verified on 1, 100, and 10k structures with autobatching — all work now and symmetries go up, i did not encounter a case where symmetry broke |
lbfgs_step was setting state.row_vector_cell directly, bypassing set_constrained_cell() which applies cell constraints like FixSymmetry. This meant FixSymmetry's adjust_cell (which symmetrizes the deformation gradient) was never called during LBFGS cell optimization. Fix: compute new_col_vector_cell and use set_constrained_cell() with scale_atoms=True, matching the pattern already used in fire_step. Tested on 1 and 100 structures with MACE+cueq LBFGS + FixSymmetry. Co-authored-by: Cursor <cursoragent@cursor.com>
|
found another issue (cc @abhijeetgangan) |
- test_merge_multi_system_constraints_no_duplicate_indices: merges a 3-system and 2-system FixSymmetry constraint and asserts sequential system_idx [0,1,2,3,4] — the old state_indices-based offset produced duplicates [0,1,2,1,2] which triggered SystemConstraint validation - test_lbfgs_cell_optimization_preserves_symmetry: runs LBFGS with cell_filter + FixSymmetry on a BCC structure with noisy forces and asserts spacegroup is preserved — the old lbfgs_step bypassed set_constrained_cell so FixSymmetry.adjust_cell was never called - make adjust_stress/adjust_cell default no-ops in Constraint base class instead of @AbstractMethod, removing 4 boilerplate overrides in FixAtoms/FixCom that did nothing - remove __all__ from symmetrize.py - remove unused OptimizationResult TypedDict, structure_with_spacegroup fixture, and unnecessary if-guards on constraint loops in cell_filters Co-authored-by: Cursor <cursoragent@cursor.com>
- replace all spglib usage with moyopy (moyo's Python bindings) for symmetry detection, refinement, and dataset queries - rewrite refine_symmetry using metric tensor polar decomposition + periodic-aware position averaging (no longer depends on spglib's standardized cell pipeline) - FixSymmetry.get_removed_dof returns 0 tensor instead of raising NotImplementedError so temperature calculations work in MD - rename sys_idx_local/sys_idx_global to constraint_idx/sys_idx for clarity (thomasloux review) - add cross-reference to transforms.get_fractional_coordinates in _get_scaled_positions docstring - document n_ops meaning and unbatched nature in symmetrize.py module docstring Co-authored-by: Cursor <cursoragent@cursor.com>
|
i think 73532ac addressed most (or all) of the outstanding PR comments
|
|
Thanks everyone! Let me know if there's anything left for me to do. |
- Rewrite symmetrize.py: inline trivial helpers, extract shared moyopy logic into _extract_symmetry_ops and _refine_symmetry_impl - Add chunked fallback in build_symmetry_map for large systems (>200 atoms) to avoid O(n_ops * n_atoms^2) OOM - Add refine_and_prep_symmetry() to combine refinement + symmetry detection in a single moyopy call, eliminating redundant C-library invocation - Lazy-import symmetrize functions inside FixSymmetry methods so importing constraints.py for FixAtoms/FixCom doesn't load the symmetry module - Simplify FixSymmetry.adjust_cell by removing confusing .mT transpose dance - Revert unrelated _system_mapping renames in model files - Add missing tests: position symmetrization vs ASE, refine_symmetry correctness, moyopy to pyproject.toml symmetry extra Co-authored-by: Cursor <cursoragent@cursor.com>
|
while doing some cleanup/refactoring, i just noticed that |
…em states SystemConstraint.merge used the raw state enumeration index as the system offset, which is only correct when each state has exactly 1 system. For multi-system states (e.g. merging two FixCom([0,1]) constraints), this produced duplicate indices [0,1,1,2] instead of [0,1,2,3]. Fix: merge_constraints now computes cumulative system offsets from num_systems_per_state (passed from concatenate_states) and uses those as the state_indices for SystemConstraint.merge, so the offset correctly accounts for multi-system states and gaps from states without the constraint. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@janosh Could you add some tests for that? Would be great, this is clearly something we don't want to mess up ! |
The old merge(constraints, state_indices, atom_offsets) conflated two concerns: shifting indices to global coordinates and concatenating constraints. The state_indices parameter meant different things for different constraint types, and FixSymmetry ignored it entirely. New design: - reindex(atom_offset, system_offset): returns a copy with indices shifted to global coordinates. Each subclass knows what to shift. - merge(constraints): just concatenates already-reindexed constraints. No offset logic needed. - merge_constraints() orchestrates: reindex first, then merge. This eliminates the ambiguous state_indices parameter and gives each method a single clear responsibility. Co-authored-by: Cursor <cursoragent@cursor.com>
|
seems like the current |
New tests covering key fixes: - reindex() preserves rotations/symm_maps while shifting system_idx - SystemConstraint.merge multi-system duplicate-indices regression - concatenate_states end-to-end with FixSymmetry and FixCom - FixSymmetry.__init__ validation, select returning None - adjust_positions/adjust_cell skip when disabled - build_symmetry_map chunked vs vectorized path equivalence - refine_symmetry recovers correct spacegroup after perturbation - position symmetrization matches ASE Simplifications: - Extract p6bar_both_constraints fixture (dedup 4 ASE comparison tests) - Merge 2 test classes into TestFixSymmetryMergeSelectReindex - Combine similar tests via parametrize and multi-assert - Unwrap single-test class, add CPU constant Co-authored-by: Cursor <cursoragent@cursor.com>
- fix reindex() sharing mutable rotations/symm_maps lists between original and copy (shallow copy with list()) - validate adjust_positions/adjust_cell flag consistency in merge() - catch NaN in deformation guard via negated comparison (NaN > x is False) - clamp eigenvalues in _mat_sqrt to prevent NaN from float noise - use torch.linalg.solve instead of inv @ matmul for numerical stability - extract _cumsum_with_zero utility to deduplicate 4 identical patterns - skip stress clone in _get_constrained_stress when no constraints - make Constraint.merge @AbstractMethod (was non-abstract NotImplementedError) - use mask.nonzero() instead of manual list comprehension in select_constraint - add type annotations to NoisyModelWrapper test helper Co-authored-by: Cursor <cursoragent@cursor.com>
janosh
left a comment
There was a problem hiding this comment.
looks in good shape to me now. @thomasloux added bunch more tests. thanks again @danielzuegner 👍
Summary
This PR adds a
FixSymmetryconstraint inspired by the wayASEimplements it. Disclaimer: I used an LLM to support this PR, and vetted the code quite thoroughly.Checklist
Before a pull request can be merged, the following items must be checked: