|
10 | 10 | Path, |
11 | 11 | ) |
12 | 12 |
|
| 13 | +import numpy as np |
13 | 14 | import torch |
14 | 15 |
|
15 | 16 | from deepmd.pt.entrypoints.main import ( |
@@ -608,5 +609,122 @@ def tearDown(self) -> None: |
608 | 609 | DPTrainTest.tearDown(self) |
609 | 610 |
|
610 | 611 |
|
| 612 | +class TestModelChangeOutBiasFittingStat(unittest.TestCase): |
| 613 | + """Verify model_change_out_bias produces the same fitting stat as the old code path. |
| 614 | +
|
| 615 | + The old code called compute_fitting_input_stat inside change_out_bias (make_model.py). |
| 616 | + The new code calls get_fitting_net().compute_input_stats() separately in |
| 617 | + model_change_out_bias (training.py). This test verifies they produce identical |
| 618 | + out_bias, fparam_avg, and fparam_inv_std. |
| 619 | + """ |
| 620 | + |
| 621 | + def test_fitting_stat_consistency(self) -> None: |
| 622 | + from deepmd.pt.model.model import get_model as get_model_pt |
| 623 | + from deepmd.pt.model.model.ener_model import EnergyModel as EnergyModelPT |
| 624 | + from deepmd.pt.train.training import ( |
| 625 | + model_change_out_bias, |
| 626 | + ) |
| 627 | + from deepmd.pt.utils.utils import to_numpy_array as torch_to_numpy |
| 628 | + from deepmd.pt.utils.utils import to_torch_tensor as numpy_to_torch |
| 629 | + from deepmd.utils.argcheck import model_args as model_args_fn |
| 630 | + |
| 631 | + # Build a model with numb_fparam=2 so fitting stat is non-trivial |
| 632 | + model_params = model_args_fn().normalize_value( |
| 633 | + { |
| 634 | + "type_map": ["O", "H"], |
| 635 | + "descriptor": { |
| 636 | + "type": "se_e2_a", |
| 637 | + "sel": [20, 20], |
| 638 | + "rcut_smth": 0.50, |
| 639 | + "rcut": 6.00, |
| 640 | + "neuron": [3, 6], |
| 641 | + "resnet_dt": False, |
| 642 | + "axis_neuron": 2, |
| 643 | + "precision": "float64", |
| 644 | + "type_one_side": True, |
| 645 | + "seed": 1, |
| 646 | + }, |
| 647 | + "fitting_net": { |
| 648 | + "neuron": [5, 5], |
| 649 | + "resnet_dt": True, |
| 650 | + "precision": "float64", |
| 651 | + "seed": 1, |
| 652 | + "numb_fparam": 2, |
| 653 | + }, |
| 654 | + }, |
| 655 | + trim_pattern="_*", |
| 656 | + ) |
| 657 | + |
| 658 | + # Create two identical models via serialize/deserialize |
| 659 | + model_orig = get_model_pt(model_params) |
| 660 | + serialized = model_orig.serialize() |
| 661 | + model_a = EnergyModelPT.deserialize(deepcopy(serialized)) |
| 662 | + model_b = EnergyModelPT.deserialize(deepcopy(serialized)) |
| 663 | + |
| 664 | + # Build mock stat data with fparam |
| 665 | + nframes = 4 |
| 666 | + natoms = 6 |
| 667 | + coords = np.random.default_rng(42).random((nframes, natoms, 3)) * 13.0 |
| 668 | + atype = np.array([[0, 0, 1, 1, 1, 1]] * nframes, dtype=np.int32) |
| 669 | + box = np.tile( |
| 670 | + np.eye(3, dtype=np.float64).reshape(1, 3, 3) * 13.0, (nframes, 1, 1) |
| 671 | + ) |
| 672 | + natoms_data = np.array([[6, 6, 2, 4]] * nframes, dtype=np.int32) |
| 673 | + energy = np.array([10.0, 20.0, 15.0, 25.0]).reshape(nframes, 1) |
| 674 | + # fparam with varying values so mean != 0 and std != 0 |
| 675 | + fparam = np.array( |
| 676 | + [[1.0, 3.0], [5.0, 7.0], [2.0, 8.0], [6.0, 4.0]], dtype=np.float64 |
| 677 | + ) |
| 678 | + |
| 679 | + merged = [ |
| 680 | + { |
| 681 | + "coord": numpy_to_torch(coords), |
| 682 | + "atype": numpy_to_torch(atype), |
| 683 | + "atype_ext": numpy_to_torch(atype), |
| 684 | + "box": numpy_to_torch(box), |
| 685 | + "natoms": numpy_to_torch(natoms_data), |
| 686 | + "energy": numpy_to_torch(energy), |
| 687 | + "find_energy": np.float32(1.0), |
| 688 | + "fparam": numpy_to_torch(fparam), |
| 689 | + "find_fparam": np.float32(1.0), |
| 690 | + } |
| 691 | + ] |
| 692 | + |
| 693 | + # Model A: simulate the OLD code path |
| 694 | + # old change_out_bias called both bias adjustment + compute_fitting_input_stat |
| 695 | + model_a.change_out_bias(merged, bias_adjust_mode="set-by-statistic") |
| 696 | + model_a.atomic_model.compute_fitting_input_stat(merged) |
| 697 | + |
| 698 | + # Model B: use the NEW code path via model_change_out_bias |
| 699 | + sample_func = lambda: merged # noqa: E731 |
| 700 | + model_change_out_bias(model_b, sample_func, "set-by-statistic") |
| 701 | + |
| 702 | + # Compare out_bias |
| 703 | + bias_a = torch_to_numpy(model_a.get_out_bias()) |
| 704 | + bias_b = torch_to_numpy(model_b.get_out_bias()) |
| 705 | + np.testing.assert_allclose(bias_a, bias_b, rtol=1e-10, atol=1e-10) |
| 706 | + |
| 707 | + # Compare fparam_avg and fparam_inv_std |
| 708 | + fit_a = model_a.get_fitting_net() |
| 709 | + fit_b = model_b.get_fitting_net() |
| 710 | + fparam_avg_a = torch_to_numpy(fit_a.fparam_avg) |
| 711 | + fparam_avg_b = torch_to_numpy(fit_b.fparam_avg) |
| 712 | + fparam_inv_std_a = torch_to_numpy(fit_a.fparam_inv_std) |
| 713 | + fparam_inv_std_b = torch_to_numpy(fit_b.fparam_inv_std) |
| 714 | + |
| 715 | + np.testing.assert_allclose(fparam_avg_a, fparam_avg_b, rtol=1e-10, atol=1e-10) |
| 716 | + np.testing.assert_allclose( |
| 717 | + fparam_inv_std_a, fparam_inv_std_b, rtol=1e-10, atol=1e-10 |
| 718 | + ) |
| 719 | + |
| 720 | + # Verify non-trivial: avg should not be zeros, inv_std should not be ones |
| 721 | + assert not np.allclose(fparam_avg_a, 0.0), ( |
| 722 | + "fparam_avg is still zero — stat was not computed" |
| 723 | + ) |
| 724 | + assert not np.allclose(fparam_inv_std_a, 1.0), ( |
| 725 | + "fparam_inv_std is still ones — stat was not computed" |
| 726 | + ) |
| 727 | + |
| 728 | + |
611 | 729 | if __name__ == "__main__": |
612 | 730 | unittest.main() |
0 commit comments