@@ -236,5 +236,122 @@ def tearDown(self) -> None:
236236 DPTrainTest .tearDown (self )
237237
238238
239+ class TestModelChangeOutBiasFittingStat (unittest .TestCase ):
240+ """Verify model_change_out_bias produces the same fitting stat as the old code path.
241+
242+ The old code called compute_fitting_input_stat inside change_out_bias (make_model.py).
243+ The new code calls get_fitting_net().compute_input_stats() separately in
244+ model_change_out_bias (training.py). This test verifies they produce identical
245+ out_bias, fparam_avg, and fparam_inv_std.
246+ """
247+
248+ def test_fitting_stat_consistency (self ) -> None :
249+ from deepmd .pd .model .model import get_model as get_model_pd
250+ from deepmd .pd .model .model .ener_model import EnergyModel as EnergyModelPD
251+ from deepmd .pd .train .training import (
252+ model_change_out_bias ,
253+ )
254+ from deepmd .pd .utils .utils import to_numpy_array as paddle_to_numpy
255+ from deepmd .pd .utils .utils import to_paddle_tensor as numpy_to_paddle
256+ from deepmd .utils .argcheck import model_args as model_args_fn
257+
258+ # Build a model with numb_fparam=2 so fitting stat is non-trivial
259+ model_params = model_args_fn ().normalize_value (
260+ {
261+ "type_map" : ["O" , "H" ],
262+ "descriptor" : {
263+ "type" : "se_e2_a" ,
264+ "sel" : [20 , 20 ],
265+ "rcut_smth" : 0.50 ,
266+ "rcut" : 6.00 ,
267+ "neuron" : [3 , 6 ],
268+ "resnet_dt" : False ,
269+ "axis_neuron" : 2 ,
270+ "precision" : "float64" ,
271+ "type_one_side" : True ,
272+ "seed" : 1 ,
273+ },
274+ "fitting_net" : {
275+ "neuron" : [5 , 5 ],
276+ "resnet_dt" : True ,
277+ "precision" : "float64" ,
278+ "seed" : 1 ,
279+ "numb_fparam" : 2 ,
280+ },
281+ },
282+ trim_pattern = "_*" ,
283+ )
284+
285+ # Create two identical models via serialize/deserialize
286+ model_orig = get_model_pd (model_params )
287+ serialized = model_orig .serialize ()
288+ model_a = EnergyModelPD .deserialize (deepcopy (serialized ))
289+ model_b = EnergyModelPD .deserialize (deepcopy (serialized ))
290+
291+ # Build mock stat data with fparam
292+ nframes = 4
293+ natoms = 6
294+ coords = np .random .default_rng (42 ).random ((nframes , natoms , 3 )) * 13.0
295+ atype = np .array ([[0 , 0 , 1 , 1 , 1 , 1 ]] * nframes , dtype = np .int32 )
296+ box = np .tile (
297+ np .eye (3 , dtype = np .float64 ).reshape (1 , 3 , 3 ) * 13.0 , (nframes , 1 , 1 )
298+ )
299+ natoms_data = np .array ([[6 , 6 , 2 , 4 ]] * nframes , dtype = np .int32 )
300+ energy = np .array ([10.0 , 20.0 , 15.0 , 25.0 ]).reshape (nframes , 1 )
301+ # fparam with varying values so mean != 0 and std != 0
302+ fparam = np .array (
303+ [[1.0 , 3.0 ], [5.0 , 7.0 ], [2.0 , 8.0 ], [6.0 , 4.0 ]], dtype = np .float64
304+ )
305+
306+ merged = [
307+ {
308+ "coord" : numpy_to_paddle (coords ),
309+ "atype" : numpy_to_paddle (atype ),
310+ "atype_ext" : numpy_to_paddle (atype ),
311+ "box" : numpy_to_paddle (box ),
312+ "natoms" : numpy_to_paddle (natoms_data ),
313+ "energy" : numpy_to_paddle (energy ),
314+ "find_energy" : np .float32 (1.0 ),
315+ "fparam" : numpy_to_paddle (fparam ),
316+ "find_fparam" : np .float32 (1.0 ),
317+ }
318+ ]
319+
320+ # Model A: simulate the OLD code path
321+ # old change_out_bias called both bias adjustment + compute_fitting_input_stat
322+ model_a .change_out_bias (merged , bias_adjust_mode = "set-by-statistic" )
323+ model_a .atomic_model .compute_fitting_input_stat (merged )
324+
325+ # Model B: use the NEW code path via model_change_out_bias
326+ sample_func = lambda : merged # noqa: E731
327+ model_change_out_bias (model_b , sample_func , "set-by-statistic" )
328+
329+ # Compare out_bias
330+ bias_a = paddle_to_numpy (model_a .get_out_bias ())
331+ bias_b = paddle_to_numpy (model_b .get_out_bias ())
332+ np .testing .assert_allclose (bias_a , bias_b , rtol = 1e-10 , atol = 1e-10 )
333+
334+ # Compare fparam_avg and fparam_inv_std
335+ fit_a = model_a .get_fitting_net ()
336+ fit_b = model_b .get_fitting_net ()
337+ fparam_avg_a = paddle_to_numpy (fit_a .fparam_avg )
338+ fparam_avg_b = paddle_to_numpy (fit_b .fparam_avg )
339+ fparam_inv_std_a = paddle_to_numpy (fit_a .fparam_inv_std )
340+ fparam_inv_std_b = paddle_to_numpy (fit_b .fparam_inv_std )
341+
342+ np .testing .assert_allclose (fparam_avg_a , fparam_avg_b , rtol = 1e-10 , atol = 1e-10 )
343+ np .testing .assert_allclose (
344+ fparam_inv_std_a , fparam_inv_std_b , rtol = 1e-10 , atol = 1e-10
345+ )
346+
347+ # Verify non-trivial: avg should not be zeros, inv_std should not be ones
348+ assert not np .allclose (fparam_avg_a , 0.0 ), (
349+ "fparam_avg is still zero — stat was not computed"
350+ )
351+ assert not np .allclose (fparam_inv_std_a , 1.0 ), (
352+ "fparam_inv_std is still ones — stat was not computed"
353+ )
354+
355+
239356if __name__ == "__main__" :
240357 unittest .main ()
0 commit comments