LES extension (dipole, quadrupoles, polarizability, etc.) merge clean - #1478
LES extension (dipole, quadrupoles, polarizability, etc.) merge clean#1478djkim-kr wants to merge 62 commits into
Conversation
Clean tp5
Clean tp5
…zability behavior
Clean l1l2 quads clean
…MACELES/LES Merge commit resolving conflicts in 5 files using resolutions validated on branch update-upstream-compat (both fit.sh and bec_test_original.sh pass): Conflict resolutions: - mace/modules/models.py: upstream base + AtomicForcesMACE + fix num_interactions==1 guard (add `not keep_last_layer_irreps`) - mace/modules/extensions.py: keep MACELES (with keep_last_layer_irreps=True and zero external_field→None fix) + upstream PolarMACE - mace/modules/symmetric_contraction.py: upstream + our EmptyParam.__deepcopy__ - mace/calculators/mace.py: upstream + BEC/external field support - mace/data/atomic_data.py: upstream + atomic_numbers field in from_config Fork-specific additions preserved in auto-merged files: - mace/modules/blocks.py: LinearLesReadoutBlock / NonLinearLesReadoutBlock - mace/modules/__init__.py: LES readout block exports + AtomicForcesMACE - mace/cli/run_train.py: AtomicForcesMACE training branch - mace/cli/eval_configs.py: latent_charges/dipoles/kappas/alphas output - mace/tools/arg_parser.py: AtomicForcesMACE model choice - mace/tools/model_script_utils.py: AtomicForcesMACE build block
- calculators/mace.py: insert LES_alphas, LES_kappas, BEC storage and BEC force application block after results_map processing (block was absent in upstream refactor to results_map pattern) - modules/extensions.py: add import logging; replace all bare print() calls with logging.info/debug to match mace logging conventions
- modules/models.py: remove AtomicForcesMACE class (unused, no tests) - modules/__init__.py: remove AtomicForcesMACE export - tools/arg_parser.py: remove AtomicForcesMACE from model choices - tools/model_script_utils.py: remove AtomicForcesMACE build block - cli/run_train.py: remove AtomicForcesMACE training branch - modules/symmetric_contraction.py: remove EmptyParam.__deepcopy__ (absent from upstream; PyTorch default is sufficient) - tools/train.py: remove commented-out logging artifact from merge
aacostadiaz
left a comment
There was a problem hiding this comment.
Thanks for the work here! Two things before merge: there are no tests yet, and I think the new tensor outputs give wrong numbers on the cueq backend.
| return f"{self.__class__.__name__}(scale={formatted_scale}, shift={formatted_shift})" | ||
|
|
||
| # LES-specific readout blocks | ||
| class LinearLesReadoutBlock(torch.nn.Module): |
There was a problem hiding this comment.
Could we add tests for the new outputs? A rotation/equivariance check on the new tensor readouts would be ideal
There was a problem hiding this comment.
Thank you. We now added the test for the new tensor outputs.
| n_1e = 0 | ||
| offset = 0 | ||
|
|
||
| for mul, ir in self.irreps_in: |
There was a problem hiding this comment.
With cueq the features are in ir_mul layout not mul_ir, so this reshape mixes up the vector components
There was a problem hiding this comment.
Thank you. We didn't realize that they used a different layout. Now modified the code to deal with cueq as well without problem.
| val[i] = out[key].detach() | ||
|
|
||
| if "latent_alphas" in out and out["latent_alphas"] is not None: | ||
| ret_tensors.setdefault("latent_alphas", []).append(out["latent_alphas"].detach()) |
There was a problem hiding this comment.
I think this breaks with more than one model. These are python lists and the val[i] = ... loop above does list[1] = ... on the second model we will get indexerror. Preallocate them like the other outputs.
There was a problem hiding this comment.
Thank you. Now we preallocate them like the ohter outputs
| if self.external_field is not None and getattr(self, "compute_bec", False) and "bec" in self.results: | ||
| bec_output = self.results["bec"] # [N_atoms, 2, 3, 3] or [N_atoms, 3, 3] | ||
| if bec_output.ndim == 4: | ||
| if bec_output.shape[0] == 2: |
There was a problem hiding this comment.
This looks fragile, for a 2-atom system shape[0] == 2 matches the atom axis and we sum over atoms. From what I can see les always stacks the contributions on dim 1, so isn't it simpler to just sum over dim 1 when ndim == 4?
There was a problem hiding this comment.
True. We followed your suggestion. Thank you.
| ): | ||
| super().__init__() | ||
| self.irreps_in = irreps_in | ||
| self.cdim = int(str(irreps_in).split("x")[0]) |
There was a problem hiding this comment.
This assumes all irreps have the same multiplicity and come ordered [0e, 1o, ...]. Instead of hard-coding cdim, loop over irreps_in and record the (start, end) of each 0e and 1o block, like LinearLesReadoutBlock does in its init.
There was a problem hiding this comment.
Thank you. Now it is more flexible as we loop over irreps_in and record the (start, end) of each 0e and 1o block instead of hard-coding them.
|
@cursor review |
Bugbot couldn't runBugbot is not enabled for your user on this team. Ask your team administrator to increase your team's hard limit for Bugbot seats or add you to the allowlist in the Cursor dashboard. |
Changes addressing review feedbackThank you for the prompt review!
+++ |
Add MACELES: long-range electrostatics implementation with added features (dipoles, quadrupoles, polarizability, etc.)
mace/modules/extensions.py— MACELES classclass MACELES(line 123)MACELES augments the model wiht per-atom electrostatic
degrees of freedom predicted from the equivariant node features and
passing them to the
leslibrary to calculate long-range part.This PR adds extended features including dipoles, quadrupoles, polarizability, etc.
Readout architecture (
__init__, lines 124–226)Each quantity uses a dedicated readout module cloned from the MACE readout
stack via two helper functions:
_copy_mace_readout(readout, change_irrep_out=...)(line 50): extendedfrom upstream's version with an optional
change_irrep_outargument.Used to clone the existing readout and retarget its output irreps —
e.g.
change_irrep_out="1x1o"for dipoles,"1x2e"for quadrupoles._copy_mace_readout_tp(readout, ...)(line 77, new): builds aLinearLesReadoutBlockorNonLinearLesReadoutBlockfor quantitiesthat require an outer product of l=1 features (see
blocks.pybelow).The per-quantity readouts are:
les_readouts)0eles_u_readouts)change_irrep_out="1x1o"1oles_quad_2e_readouts)change_irrep_out="1x2e", then converted viachange_of_basis_quads(ReducedTensorProducts('ij=ji', i='1o')) to Cartesian 3×32eles_quad_1o_readouts)_copy_mace_readout_tp→LinearLesReadoutBlock1o ⊗ 1oles_alpha_readouts)0eles_alpha_2e_readouts)change_irrep_out="1x0e + 1x2e", converted viaCartesianTensor("ij=ji").reduced_tensor_products().change_of_basis0e + 2eles_alpha_1o_readouts)_copy_mace_readout_tp→LinearLesReadoutBlock1o ⊗ 1oles_kappa_readouts)0eThe l=1 outer-product path (
les_quad_1o_readouts,les_alpha_1o_readouts)exists because many trained MACE models use only l≤1 hidden irreps and
therefore have no
2efeatures — the quadrupole/polarizability tensormust then be formed as v⊗v from the available
1ochannel.keep_last_layer_irrepsrequirement (line 126)MACELES forces this flag before calling
super().__init__(). Without it,MACE.__init__would collapse the last interaction layer to scalar-onlyoutput (see
models.pyfix below), making vector/tensor featuresunavailable at readout time.
mace/modules/blocks.py— LES readout blocks (lines 1400–1555)LinearLesReadoutBlock(line 1400)Predicts a per-atom 3×3 symmetric polarizability tensor from equivariant
features. For each node:
Linearlayer mixing features within the inputirreps.
0e) weights αᵢₖ and vector (1o/1e) channels vᵢₖ.scalar_to_weight_1o/scalar_to_weight_1elayer.T = Σₖ wₖ · (vₖ ⊗ vₖ)— this isautomatically symmetric and equivariant (SO(3)-covariant under
rotations of the input features).
(
make_w_pos=True).The outer product lives entirely in standard PyTorch (no e3nn CG
coefficients needed for the contraction), keeping compilation overhead low.
NonLinearLesReadoutBlock(line 1502)Nonlinear variant: passes the equivariant features through a gated
nonlinearity (
e3nn.nn.Gate) before the same outer-product readout.Used when the linear model undershoots the polarizability dynamic range.
mace/modules/models.py(line 169)When
num_interactions=1, upstream unconditionally reduced the hiddenirreps to scalars only, even when
keep_last_layer_irreps=True. MACELESsets this flag to preserve vector/tensor features; without the fix,
hidden_irreps_outbecomes e.g."128x0e"instead of the full irreps,and the downstream LES readout blocks raise error due to lack of L=1 or L=2 node features.
mace/data/atomic_data.py—atomic_numbersfield (line 402)Required for future use of element-specific fixed charge options in the LES
library (e.g.
use_fixed_atomic_charges).AtomicData.from_configwas not populating this field, causing aKeyErrorwhen those LES options are active.mace/calculators/mace.py— BEC forces under external electric fields and LES outputsMACECalculator.__init__(line ~112)Five new parameters:
compute_bec,external_field,eps_infty,electric_field_unit,keep_neutral. Whencompute_bec=True,"bec"is added to
implemented_properties.MACECalculator.calculate— forward pass (lines 644–683)compute_bec=Trueis forwarded viaforward_kwargs.The LES latent outputs (
latent_alphas,latent_kappas,BEC) arecollected into Python lists (
ret_tensors.setdefault(..., []).append(...)),so they are accessible as ASE calculator results during MD.
MACECalculator.calculate— post-processing (lines 760–803)After the
results_mapblock, the list-accumulated LES quantities areaveraged over ensemble models and stored:
When
external_fieldis set, atomic forces are augmented by the BECcontribution:
keep_neutral=Truesubtracts the mean BEC before the force applicationto enforce acoustic sum rule neutrality.
More details are described in our preprint.
mace/cli/eval_configs.py— latent quantity outputrun(): added collection listsus_list,kappas_list,alphas_listalongside the existing
qs_list. Per-structure latent quantities aresplit from the batch using
batch.ptrand written toatoms.arraysas{prefix}latent_dipoles,{prefix}latent_kappas,{prefix}latent_alphas.This enables inspection of the internal LES state without modifying the
model forward pass.
Backward compatibility
With this branch, training scripts and pre-trained
MACELESpotentials fromChengUCB/mace:mainload and run without modification. (The training scripts and potentials areshared in https://github.com/ChengUCB/extended_les_fit/tree/main/MLIPs/MACE-LES)
MACELES.forwardwraps attribute accesses that wereadded in later versions (
les_output_scale,les_kappa_scale,les_alpha_scale,use_anisotropic_polarizability) withhasattrguardsto handle older checkpoints.