3131 LRFittingNet ,
3232)
3333
34- SOG_DEFAULT_B = to_numpy_array (np .array (1.62976708826776469 ))
34+ SOG_DEFAULT_B = to_numpy_array (np .array (2.0 ))
3535SOG_DEFAULT_SIGMA = to_numpy_array (np .array (2.180230445405648 ))
3636SOG_DEFAULT_M = int (12 )
37+ # r_cut / sigma = RCUT_TO_SIGMA for C¹ continuity at the cutoff.
38+ # Equivalent to the parameter in sog.module.gaussian.
39+ RCUT_TO_SIGMA = 1.9892536839080267
3740
3841
3942@LRFittingNet .register ("sog_energy" )
@@ -111,6 +114,10 @@ class SOGEnergyFittingNet(LRFittingNet):
111114 external_kspace : bool
112115 If True, long-range correction is handled externally (e.g. kspace),
113116 and the model only provides latent charges.
117+ use_cubes2_fft : bool
118+ If True, use CubeS₂ + FFT for long-range computation (fast, default).
119+ If False, use direct k-space summation (exact but slower for large systems).
120+ The grid resolution is controlled by n_dl (k-space cutoff) when FFT is off.
114121 """
115122
116123 def __init__ (
@@ -147,7 +154,9 @@ def __init__(
147154 n_dl : float | int | None = None ,
148155 cubes2_phi_max : float | None = None ,
149156 remove_self_interaction : bool = False ,
157+ charge_neutral_lambda : float | None = None ,
150158 external_kspace : bool = False ,
159+ use_cubes2_fft : bool = False ,
151160 ** kwargs : Any ,
152161 ) -> None :
153162 super ().__init__ (
@@ -185,8 +194,11 @@ def __init__(
185194 if b_value <= 0.0 :
186195 raise ValueError ("`b` should be positive." )
187196
197+ self ._sigma_user_set = sigma is not None
198+ self .use_cubes2_fft = bool (use_cubes2_fft )
199+ self .charge_neutral_lambda = charge_neutral_lambda
188200 if sigma is None :
189- sigma_value = SOG_DEFAULT_SIGMA # will be overridden by sog lib via rcut
201+ sigma_value = float ( SOG_DEFAULT_SIGMA ) # placeholder, may be recomputed via rcut
190202 else :
191203 sigma_tensor = torch .as_tensor (sigma , dtype = dtype , device = device )
192204 sigma_value = float (sigma_tensor .reshape (- 1 )[0 ].item ())
@@ -297,6 +309,9 @@ def serialize(self) -> dict:
297309 data ["n_dl" ] = self .n_dl # legacy
298310 data ["remove_self_interaction" ] = bool (self .remove_self_interaction )
299311 data ["external_kspace" ] = bool (self .external_kspace )
312+ data ["use_cubes2_fft" ] = bool (self .use_cubes2_fft )
313+ if self .charge_neutral_lambda is not None :
314+ data ["charge_neutral_lambda" ] = self .charge_neutral_lambda
300315 return data
301316
302317 @classmethod
@@ -311,6 +326,8 @@ def deserialize(cls, data: dict) -> "SOGEnergyFittingNet":
311326
312327 obj = super ().deserialize (data )
313328
329+ obj .charge_neutral_lambda = data .get ("charge_neutral_lambda" , None )
330+
314331 with torch .no_grad ():
315332 if bandwidth_tensor is not None :
316333 bw = bandwidth_tensor .to (
@@ -364,6 +381,39 @@ def deserialize(cls, data: dict) -> "SOGEnergyFittingNet":
364381 def _kernel_params (self ) -> tuple [torch .Tensor , torch .Tensor ]:
365382 return self .amp , self .bandwidth
366383
384+ def recompute_from_rcut (self , rcut : float , nlayers : int = 1 ) -> None :
385+ """Recompute sigma, amp, bandwidth from the descriptor's r_cut.
386+
387+ This implements the SOG library's default sigma formula:
388+ sigma = r_cut * nlayers / RCUT_TO_SIGMA
389+
390+ Only recomputes when sigma was NOT explicitly set by the user,
391+ so explicit sigma in the config is always respected.
392+ """
393+ if self ._sigma_user_set :
394+ return # user explicitly set sigma — don't override
395+
396+ new_sigma = rcut * nlayers / RCUT_TO_SIGMA
397+ b_base = torch .tensor (self .b , dtype = self .amp .dtype , device = self .amp .device )
398+ bw_tensor = new_sigma * torch .pow (
399+ b_base ,
400+ torch .arange (self .M , dtype = self .amp .dtype , device = self .amp .device ),
401+ )
402+ new_bandwidth = bw_tensor .square ()
403+ coef1 = float (4.0 * np .pi * np .log (self .b ))
404+ new_amp = torch .full_like (new_bandwidth , coef1 )
405+ new_amp *= new_bandwidth # convert to sog-lib internal amplitude
406+
407+ self .sigma = new_sigma
408+ self .amp = torch .nn .Parameter (
409+ new_amp .to (device = self .amp .device , dtype = self .amp .dtype ),
410+ requires_grad = bool (self .trainable ),
411+ )
412+ self .bandwidth = torch .nn .Parameter (
413+ new_bandwidth .to (device = self .bandwidth .device , dtype = self .bandwidth .dtype ),
414+ requires_grad = bool (self .trainable ),
415+ )
416+
367417 def forward (
368418 self ,
369419 descriptor : torch .Tensor ,
0 commit comments