Repository: uf3/uf3 (language: python)

Issue:
Using LAMMPS
- Instructions for setting up ASE LAMMPS variable ('ASE_LAMMPSRUN_COMMAND')


Discussion / hints:

Fix patch:
diff --git a/uf3/representation/bspline.py b/uf3/representation/bspline.py
index 901c143c..1b74e15d 100644
--- a/uf3/representation/bspline.py
+++ b/uf3/representation/bspline.py
@@ -234,17 +234,14 @@ def update_knots(self,
             self.r_max_map[trio] = self.r_max_map.get(trio, default_max)
             self.resolution_map[trio] = self.resolution_map.get(trio,
                                                                 default_res)
-            min_set = len(set(self.r_min_map[trio]))
-            max_set = len(set(self.r_max_map[trio]))
-            res_set = len(set(self.resolution_map[trio]))
-            if min_set == 1 and max_set == 1 and res_set == 1:
-                self.symmetry[trio] = 3
-            elif min_set <= 2 and max_set <= 2 and res_set <= 2:
-                self.symmetry[trio] = 2
-            else:
-                self.symmetry[trio] = 1
+            self.symmetry[trio] = find_symmetry_3B(trio,
+                                                   self.r_min_map[trio],
+                                                   self.r_max_map[trio],
+                                                   self.resolution_map[trio])
+            
         self.r_cut = self.get_cutoff()
 
+
     def update_knots_from_dict(self, knots_map):
         """"""
         for pair in self.interactions_map.get(2, []):
@@ -560,11 +557,11 @@ def compress_3B(self, grid, interaction, fitting = True):
 
         """
         if self.symmetry[interaction] == 1:
-            vec = grid.flatten()
+            vec = grid
+            redundancy = self.flat_weights[interaction] if fitting else 1.0
         elif self.symmetry[interaction] == 2:
             vec = grid + grid.transpose(1, 0, 2)
-            vec = vec.flat[self.template_mask[interaction]]
-            vec = vec * (self.flat_weights[interaction] if fitting else 0.5)
+            redundancy = self.flat_weights[interaction] if fitting else 0.5
         elif self.symmetry[interaction] == 3:
             vec = (grid
                    + grid.transpose(0, 2, 1)
@@ -572,10 +569,12 @@ def compress_3B(self, grid, interaction, fitting = True):
                    + grid.transpose(1, 2, 0)
                    + grid.transpose(2, 0, 1)
                    + grid.transpose(2, 1, 0))
-            vec = vec.flat[self.template_mask[interaction]]
-            vec = vec * self.flat_weights[interaction]
+            redundancy = self.flat_weights[interaction] if fitting else 1/6
+        vec = vec.flat[self.template_mask[interaction]]
+        vec = vec * redundancy
         return vec
 
+
     def decompress_3B(self, vec, interaction):
         """
 
@@ -593,10 +592,62 @@ def decompress_3B(self, vec, interaction):
         N = len(n_space) - 4
         grid = np.zeros((L, M, N))
         grid.flat[self.template_mask[interaction]] = vec
-        grid = grid + grid.transpose(1, 0, 2)
+        if self.symmetry[interaction] == 2:
+            grid = grid + grid.transpose(1, 0, 2)
+        elif self.symmetry[interaction] == 3:
+            grid = (grid
+                    + grid.transpose(0, 2, 1)
+                    + grid.transpose(1, 0, 2)
+                    + grid.transpose(1, 2, 0)
+                    + grid.transpose(2, 0, 1)
+                    + grid.transpose(2, 1, 0))
         return grid
 
 
+    
+def find_symmetry_3B(trio: Tuple, 
+                     r_min: List,
+                     r_max: List, 
+                     resolution: List):
+
+    """
+    Calculates the symmetry of a 3-body interaction based on r_min, r_max, and
+    resolution map.
+
+    Args:
+        trio (tuple): Interaction (Si,Si,N)
+        r_min (list): minimum pair distance per interaction
+            e.g. [2.0, 3.0, 4.0]
+        r_max (list): maximum pair distance per interaction
+        resolution (list): resolution (number of knot intervals) per interaction.
+    Returns:
+        symmetry (int): Symmetry W.R.T the central atom
+
+    """ 
+    # 2 neighboring elements are different
+    if trio[1] != trio[2]:
+        return 1
+    else:  # 2 neighboring elements are identical
+
+        configs = list(zip(
+            r_min,
+            r_max,
+            resolution
+        ))
+
+        if configs[0] == configs[1] == configs[2]:
+            if trio[0] == trio[1]:
+                return 3
+            else:
+                return 2
+        elif configs[0] == configs[1]:
+            return 2
+        else:
+            # 3 legs all have different configurations
+            # OR the 2 central legs have different configurations
+            return 1
+    
+    
 def get_knot_spacer(knot_strategy):
     """
 


Test patch:
diff --git a/tests/test_bsplines.py b/tests/test_bsplines.py
index 52450240..32a43584 100644
--- a/tests/test_bsplines.py
+++ b/tests/test_bsplines.py
@@ -10,7 +10,240 @@ def binary_chemistry():
     chemistry_config = composition.ChemicalSystem(element_list)
     yield chemistry_config
 
+@pytest.fixture()
+def binary_chemistry():
+    element_list = ['Ne', 'Xe']
+    chemistry_config = composition.ChemicalSystem(element_list)
+    yield chemistry_config
+
+@pytest.fixture
+def unary_trio():
+    return ('Si','Si','Si')
+
+@pytest.fixture
+def binary_sym_trio():
+    return ('Si','N','N')
+
+@pytest.fixture
+def binary_unsym_trio():
+    return ('Si','Si','N') 
+
+@pytest.fixture
+def equilateral():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [10,10,10])
+
+### The naming convention is TRIANGLE-TYPE_W.R.T_LEG ###
+@pytest.fixture
+def isosceles_rmax_rjk():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [5.,5.,10.],
+                resolution = [6,6,6])
+@pytest.fixture
+def isosceles_rmax_rij():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [5.,10.,5.],
+                resolution = [6,6,6])
+@pytest.fixture
+def isosceles_rmax_rik():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [10.,5.,5.],
+                resolution = [6,6,6])
+### Rmin iso ###
+@pytest.fixture
+def isosceles_rmin_rjk():
+    return dict(r_min = [0.1,0.1,0.2],
+                r_max = [8.,8.,8.],
+                resolution = [6,6,6])
+@pytest.fixture
+def isosceles_rmin_rij():
+    return dict(r_min = [0.2,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [6,6,6])
+@pytest.fixture
+def isosceles_rmin_rik():
+    return dict(r_min = [0.1,0.2,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [6,6,6])
+
+### Resolution iso ###
+@pytest.fixture
+def isosceles_resolution_rjk():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [6,6,12])
+@pytest.fixture
+def isosceles_resolution_rij():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [12,6,6])
+@pytest.fixture
+def isosceles_resolution_rik():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [6,12,6])
+
+@pytest.fixture
+def scalene_rmax():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [4.,5.,10.],
+                resolution = [6,6,6])
+
+@pytest.fixture
+def scalene_rmin():
+    return dict(r_min = [0.1,0.2,0.3],
+                r_max = [8.,8.,8.],
+                resolution = [6,6,6])
+
+@pytest.fixture
+def scalene_resolution():
+    return dict(r_min = [0.1,0.1,0.1],
+                r_max = [8.,8.,8.],
+                resolution = [4,6,12])
+
+@pytest.fixture
+def isosceles_rmax_resolution_scalene_rmin():
+    return dict(r_min = [0.2,0.1,0.1],
+                r_max = [5.,5.,10.],
+                resolution = [6,6,12])
+
+@pytest.fixture
+def scalene_all_diff():
+    return dict(r_min = [0.1,0.1,0.2],
+                r_max = [5.,10.,5.],
+                resolution = [10,20,20])
+
+class TestFindSym3Body:
+    def test_equilateral_unary(self,unary_trio,equilateral):
+        assert find_symmetry_3B(unary_trio,**equilateral) == 3
+        
+    def test_equilateral_binary_sym(self,binary_sym_trio,equilateral):   
+        assert find_symmetry_3B(binary_sym_trio,**equilateral) == 2 
+        
+    def test_equilateral_binary_unsym(self,binary_unsym_trio,equilateral):
+        assert find_symmetry_3B(binary_unsym_trio,**equilateral) == 1        
+        
+    def test_isosceles_unary_rmax_rjk(self,unary_trio,isosceles_rmax_rjk):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmax_rjk) == 2
+        
+    def test_isosceles_binary_sym_rmax_rjk(self,binary_sym_trio,isosceles_rmax_rjk):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmax_rjk) == 2
+        
+    def test_isosceles_binary_unsym_rmax_rjk(self,binary_unsym_trio,isosceles_rmax_rjk):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmax_rjk) == 1
+        
+    def test_isosceles_unary_rmax_rij(self,unary_trio,isosceles_rmax_rij):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmax_rij) == 1
+        
+    def test_isosceles_binary_sym_rmax_rij(self,binary_sym_trio,isosceles_rmax_rij):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmax_rij) == 1
+        
+    def test_isosceles_binary_unsym_rmax_rij(self,binary_unsym_trio,isosceles_rmax_rij):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmax_rij) == 1        
+        
+    def test_isosceles_unary_rmax_rik(self,unary_trio,isosceles_rmax_rik):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmax_rik) == 1
+        
+    def test_isosceles_binary_sym_rmax_rik(self,binary_sym_trio,isosceles_rmax_rik):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmax_rik) == 1
+        
+    def test_isosceles_binary_unsym_rmax_rik(self,binary_unsym_trio,isosceles_rmax_rik):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmax_rik) == 1
+        
+    def test_isosceles_unary_rmin_rjk(self,unary_trio,isosceles_rmin_rjk):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmin_rjk) == 2
+        
+    def test_isosceles_binary_sym_rmin_rjk(self,binary_sym_trio,isosceles_rmin_rjk):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmin_rjk) == 2
+        
+    def test_isosceles_binary_unsym_rmin_rjk(self,binary_unsym_trio,isosceles_rmin_rjk):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmin_rjk) == 1
+        
+    def test_isosceles_unary_rmin_rij(self,unary_trio,isosceles_rmin_rij):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmin_rij) == 1
+        
+    def test_isosceles_binary_sym_rmin_rij(self,binary_sym_trio,isosceles_rmin_rij):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmin_rij) == 1
+        
+    def test_isosceles_binary_unsym_rmin_rij(self,binary_unsym_trio,isosceles_rmin_rij):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmin_rij) == 1        
+        
+    def test_isosceles_unary_rmin_rik(self,unary_trio,isosceles_rmin_rik):
+        assert find_symmetry_3B(unary_trio,**isosceles_rmin_rik) == 1
+        
+    def test_isosceles_binary_sym_rmin_rik(self,binary_sym_trio,isosceles_rmin_rik):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmin_rik) == 1
+        
+    def test_isosceles_binary_unsym_rmin_rik(self,binary_unsym_trio,isosceles_rmin_rik):
+        assert find_symmetry_3B(binary_unsym_trio,**isosceles_rmin_rik) == 1   
+
+    def test_isosceles_unary_resolution_rjk(self, unary_trio, isosceles_resolution_rjk):
+        assert find_symmetry_3B(unary_trio, **isosceles_resolution_rjk) == 2
+
+    def test_isosceles_binary_sym_resolution_rjk(self, binary_sym_trio, isosceles_resolution_rjk):
+        assert find_symmetry_3B(binary_sym_trio, **isosceles_resolution_rjk) == 2
+
+    def test_isosceles_binary_unsym_resolution_rjk(self, binary_unsym_trio, isosceles_resolution_rjk):
+        assert find_symmetry_3B(binary_unsym_trio, **isosceles_resolution_rjk) == 1
+
+    def test_isosceles_unary_resolution_rij(self, unary_trio, isosceles_resolution_rij):
+        assert find_symmetry_3B(unary_trio, **isosceles_resolution_rij) == 1
+
+    def test_isosceles_binary_sym_resolution_rij(self, binary_sym_trio, isosceles_resolution_rij):
+        assert find_symmetry_3B(binary_sym_trio, **isosceles_resolution_rij) == 1
+
+    def test_isosceles_binary_unsym_resolution_rij(self, binary_unsym_trio, isosceles_resolution_rij):
+        assert find_symmetry_3B(binary_unsym_trio, **isosceles_resolution_rij) == 1
+
+    def test_isosceles_unary_resolution_rik(self, unary_trio, isosceles_resolution_rik):
+        assert find_symmetry_3B(unary_trio, **isosceles_resolution_rik) == 1
+
+    def test_isosceles_binary_sym_resolution_rik(self, binary_sym_trio, isosceles_resolution_rik):
+        assert find_symmetry_3B(binary_sym_trio, **isosceles_resolution_rik) == 1
+
+    def test_isosceles_binary_unsym_resolution_rik(self, binary_unsym_trio, isosceles_resolution_rik):
+        assert find_symmetry_3B(binary_unsym_trio, **isosceles_resolution_rik) == 1
+        
+    def test_scalene_unary_rmax(self,unary_trio,scalene_rmax):
+        assert find_symmetry_3B(unary_trio,**scalene_rmax) == 1
+        
+    def test_scalene_binary_sym_rmax(self,binary_sym_trio,scalene_rmax):
+        assert find_symmetry_3B(binary_sym_trio,**scalene_rmax) == 1
+        
+    def test_scalene_binary_unsym_rmax(self,binary_unsym_trio,scalene_rmax):
+        assert find_symmetry_3B(binary_unsym_trio,**scalene_rmax) == 1  
+        
+    def test_scalene_unary_rmin(self,unary_trio,scalene_rmin):
+        assert find_symmetry_3B(unary_trio,**scalene_rmin) == 1
+        
+    def test_scalene_binary_sym_rmin(self,binary_sym_trio,scalene_rmin):
+        assert find_symmetry_3B(binary_sym_trio,**scalene_rmin) == 1
+        
+    def test_scalene_binary_unsym_rmin(self,binary_unsym_trio,scalene_rmin):
+        assert find_symmetry_3B(binary_unsym_trio,**scalene_rmin) == 1            
+
+    def test_scalene_unary_resolution(self,unary_trio,scalene_resolution):
+        assert find_symmetry_3B(unary_trio,**scalene_resolution) == 1
+        
+    def test_scalene_binary_sym_resolution(self,binary_sym_trio,scalene_resolution):
+        assert find_symmetry_3B(binary_sym_trio,**scalene_resolution) == 1
+        
+    def test_scalene_binary_unsym_resolution(self,binary_unsym_trio,scalene_resolution):
+        assert find_symmetry_3B(binary_unsym_trio,**scalene_resolution) == 1   
+
+    def test_binary_sym_isosceles_rmax_resolution_scalene_rmin(self,binary_sym_trio,isosceles_rmax_resolution_scalene_rmin):
+        assert find_symmetry_3B(binary_sym_trio,**isosceles_rmax_resolution_scalene_rmin) == 1
+
+    def test_scalene_unary_all_diff(self,unary_trio,scalene_all_diff):
+        assert find_symmetry_3B(unary_trio,**scalene_all_diff) == 1
+
+    def test_scalene_binary_sym_all_diff(self,binary_sym_trio,scalene_all_diff):
+        assert find_symmetry_3B(binary_sym_trio,**scalene_all_diff) == 1
 
+    def test_scalene_binary_unsym_all_diff(self,binary_unsym_trio,scalene_all_diff):
+        assert find_symmetry_3B(binary_unsym_trio,**scalene_all_diff) == 1
+       
 class TestKnots:
     def test_knot_sequence_from_points(self):
         sequence = knot_sequence_from_points([1, 2, 3])