@@ -569,6 +569,15 @@ def _forward_common(
569569 fparam = fparam .to (self .prec ) if fparam is not None else None
570570 aparam = aparam .to (self .prec ) if aparam is not None else None
571571
572+ # Save target total charge from fparam (column 0) before normalization.
573+ # Assumes fparam[:, 0] stores the total charge of each frame.
574+ # If no fparam is provided, default to neutral (total charge = 0).
575+ if self .numb_fparam > 0 and fparam is not None :
576+ fparam_raw = fparam .view ([nf , self .numb_fparam ])
577+ target_total_charge = fparam_raw [:, 0 ] # [nf]
578+ else :
579+ target_total_charge = torch .zeros (nf , dtype = self .prec , device = xx .device )
580+
572581 if self .remove_vaccum_contribution is not None :
573582 xx_zeros = torch .zeros_like (xx )
574583 else :
@@ -646,6 +655,19 @@ def _forward_common(
646655 bias_tensor = None ,
647656 )
648657 lr_out = lr_out + self ._get_lr_bias (atype )
658+
659+ # Hard charge constraint: enforce sum of latent charges equals target total charge.
660+ q_mean = lr_out .mean (dim = 1 ) # [nf, lr_net_dim_out]
661+ q_target_per_atom = target_total_charge / float (nloc ) # [nf]
662+ if lr_out .shape [- 1 ] > 1 :
663+ correction = torch .zeros_like (lr_out )
664+ correction [:, :, 0 ] = q_mean [:, 0 ] - q_target_per_atom
665+ lr_out = lr_out - correction
666+ else :
667+ q_mean = q_mean .unsqueeze (1 ) # [nf, 1, 1]
668+ target = q_target_per_atom .view (nf , 1 , 1 )
669+ lr_out = lr_out - (q_mean - target )
670+
649671 mask = self .emask (atype ).to (torch .bool )
650672 sr_out = torch .where (mask [:, :, None ], sr_out , 0.0 )
651673 lr_out = torch .where (mask [:, :, None ], lr_out , 0.0 )
0 commit comments