Repository: uf3/uf3 (language: python)

Issue:
Trajectory file cannot be written with UFCalculator
This mini-script demonstrates the issue. I have an ASE Atoms object and want to run dynamics/optimization with it using the UFCalculator from UF3.

```
from uf3.forcefield import calculator
from ase import Atoms
from ase.io import trajectory
from ase.optimize.sciopt import SciPyFminCG

atoms = Atoms(...)
uf3_calc = calculator.UFCalculator(uf3_model)
atoms.calc = uf3_calc
traj = trajectory.Trajectory(traj_file, mode='w', atoms=atoms)
dyn = SciPyFminCG(atoms)
dyn.attach(traj.write)
dyn.run(steps=1)
```

The last line above raises an attribute error:

```
Traceback (most recent call last):
  File "/home/sung/UFMin/sung/data/pt_bulk/ufmin.py", line 74, in <module>
    dyn.run(steps=1, fmax=ufmin_fmax)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/sciopt.py", line 113, in run
    self.callback(None)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/sciopt.py", line 102, in callback
    self.call_observers()
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/optimize.py", line 108, in call_observers
    function(*args, **kwargs)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/io/trajectory.py", line 132, in write
    self._write_atoms(image, **kwargs)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/io/trajectory.py", line 167, in _write_atoms
    c.write(name=calc.name)
AttributeError: 'UFCalculator' object has no attribute 'name'
```

If I try to work around this by setting `calc.name` manually in my mini-script, it raises yet another attribute error:

```
Traceback (most recent call last):
  File "/home/sung/UFMin/sung/data/pt_bulk/ufmin.py", line 75, in <module>
    dyn.run(steps=1, fmax=ufmin_fmax)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/sciopt.py", line 113, in run
    self.callback(None)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/sciopt.py", line 102, in callback
    self.call_observers()
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/optimize/optimize.py", line 108, in call_observers
    function(*args, **kwargs)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/io/trajectory.py", line 132, in write
    self._write_atoms(image, **kwargs)
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/io/trajectory.py", line 169, in _write_atoms
    c.write(parameters=calc.todict())
  File "/home/sung/miniforge3/envs/ufmin/lib/python3.10/site-packages/ase/calculators/calculator.py", line 623, in todict
    for key, value in self.parameters.items():
AttributeError: 'UFCalculator' object has no attribute 'parameters'
```

I think the incorporation of UF3 into ASE should be made more complete.


Discussion / hints:

Fix patch:
diff --git a/uf3/forcefield/calculator.py b/uf3/forcefield/calculator.py
index d7216c0d..1742e378 100644
--- a/uf3/forcefield/calculator.py
+++ b/uf3/forcefield/calculator.py
@@ -41,10 +41,19 @@ class UFCalculator(ase_calc.Calculator):
     """
     ASE Calculator for energies, forces, and stresses using a fit UF potential.
     Optionally compute elastic constants and phonon spectra.
+
+    Args:
+        model (uf3.regression.WeightedLinearModel): fit model to use.
     """
+
+    implemented_properties = ['energy', 'forces']
+    implemented_properties += ['stress']  # so far numerical stress only
+    
+
     def __init__(self,
-                 model: least_squares.WeightedLinearModel):
-        # super().__init__()  # TODO: improve compatibility with ASE protocol
+                 model: least_squares.WeightedLinearModel,
+                 **kwargs):
+        super().__init__(**kwargs)
         self.bspline_config = model.bspline_config
         self.model = model
         self.solutions = coefficients_by_interaction(self.element_list,
@@ -111,10 +120,43 @@ def coefficients(self):
     def chemical_system(self):
         return self.bspline_config.chemical_system
 
-    def get_potential_energy(self,
-                             atoms: ase.Atoms = None,
-                             force_consistent: bool = None
-                             ) -> float:
+    
+    def calculate(self,
+                  atoms=None,
+                  properties=None,
+                  system_changes=tuple(ase_calc.all_changes),
+                  ):
+        """
+        Method called by `self.get_property` if it deems a new calculation
+        is required. After the calculation, the results are cached in
+        `self.results` so that these values can be fetched later if none of
+        the atoms object's attributes (i.e. positions, cell, etc.) have
+        changed since the last calculation.
+
+        Args:
+            atoms (ase.Atoms): configuration of interest.
+            properties (list): list of properties to calculate.
+                Must be a subset of `self.implemented_properties`.
+            system_changes (tuple): changes to system.
+        """
+        if properties is None:
+            properties = self.implemented_properties
+
+        ase_calc.Calculator.calculate(self, atoms, properties, system_changes)
+
+        if ('energy' in properties) or ('free_energy' in properties):
+            self.results['energy'] = self._get_potential_energy(atoms)
+            self.results['free_energy'] = self.results['energy']
+        if 'forces' in properties:
+            self.results['forces'] = self._get_forces(atoms)
+        if 'stress' in properties:
+            self.results['stress'] = self._get_stress(atoms)
+
+
+    def _get_potential_energy(self,
+                              atoms: ase.Atoms = None,
+                              force_consistent: bool = None
+                              ) -> float:
         """Evaluate the total energy of a configuration."""""
         if any(atoms.pbc):
             supercell = geometry.get_supercell(atoms, r_cut=self.r_cut)
@@ -122,23 +164,23 @@ def get_potential_energy(self,
             supercell = atoms
 
         if force_consistent is not True:
-            e_1b = self.energy_1b(atoms)
+            e_1b = self._energy_1b(atoms)
         else:
             e_1b = 0.0
 
         if self.degree >= 2:
-            e_2b = self.energy_2b(atoms, supercell)
+            e_2b = self._energy_2b(atoms, supercell)
         else:
             e_2b = 0.0
 
         if self.degree >= 3:
-            e_3b = self.energy_3b(atoms, supercell)
+            e_3b = self._energy_3b(atoms, supercell)
         else:
             e_3b = 0.0
         energy = e_1b + e_2b + e_3b
         return energy
 
-    def energy_1b(self, atoms):
+    def _energy_1b(self, atoms):
         energy = 0.0
         el_set, el_counts = np.unique(atoms.get_chemical_symbols(),
                                       return_counts=True)
@@ -146,9 +188,9 @@ def energy_1b(self, atoms):
             energy += float(self.solutions[el] * count)
         return energy
 
-    def energy_2b(self,
-                  atoms,
-                  supercell):
+    def _energy_2b(self,
+                   atoms,
+                   supercell):
         pair_tuples = self.interactions_map[2]
         distances_map = distances.distances_by_interaction(atoms,
                                                            pair_tuples,
@@ -168,9 +210,9 @@ def energy_2b(self,
             energy += energy_contribution
         return energy
 
-    def energy_3b(self,
-                  atoms,
-                  supercell):
+    def _energy_3b(self,
+                   atoms,
+                   supercell):
         energy = 0.0
 
         trio_list = self.bspline_config.interactions_map[3]
@@ -197,7 +239,7 @@ def energy_3b(self,
                 energy += np.sum(component)
         return energy
 
-    def get_forces(self, atoms: ase.Atoms = None) -> np.ndarray:
+    def _get_forces(self, atoms: ase.Atoms = None) -> np.ndarray:
         """Return the forces in a configuration."""
         n_atoms = len(atoms)
         if any(atoms.pbc):
@@ -207,18 +249,18 @@ def get_forces(self, atoms: ase.Atoms = None) -> np.ndarray:
 
         f_shape = np.zeros((n_atoms, 3))
         if self.degree >= 2:
-            f_2b = self.forces_2b(atoms, supercell)
+            f_2b = self._forces_2b(atoms, supercell)
         else:
             f_2b = np.zeros_like(f_shape)
 
         if self.degree >= 3:
-            f_3b = self.forces_3b(atoms, supercell)
+            f_3b = self._forces_3b(atoms, supercell)
         else:
             f_3b = np.zeros_like(f_shape)
         forces = f_2b + f_3b
         return forces
 
-    def forces_2b(self, atoms, supercell):
+    def _forces_2b(self, atoms, supercell):
         pair_tuples = self.interactions_map[2]
         deriv_results = distances.derivatives_by_interaction(atoms,
                                                              pair_tuples,
@@ -244,7 +286,7 @@ def forces_2b(self, atoms, supercell):
             forces -= component
         return forces
 
-    def forces_3b(self, atoms, supercell):
+    def _forces_3b(self, atoms, supercell):
         n_atoms = len(atoms)
         forces = np.zeros((n_atoms, 3))
 
@@ -347,10 +389,10 @@ def forces_3b(self, atoms, supercell):
     #         f_accumulate += np.dot(drjk_dr, val_n)
     #     return f_accumulate
 
-    def get_stress(self,
-                   atoms: ase.Atoms = None,
-                   **kwargs
-                   ) -> np.ndarray:
+    def _get_stress(self,
+                    atoms: ase.Atoms = None,
+                    **kwargs
+                    ) -> np.ndarray:
         """Return the (numerical) stress."""
         return self.calculate_numerical_stress(atoms, **kwargs)
 
@@ -388,6 +430,7 @@ def relax_fmax(self,
 
     def calculation_required(self, atoms: ase.Atoms, quantities: List) -> bool:
         """Check if a calculation is required."""
+        warnings.warn("`calculation_required` may be deprecated.")
         if any([q in quantities for q in ['magmom', 'stress', 'charges']]):
             return True
         if 'energy' in quantities and 'energy' not in atoms.info:


Test patch:
diff --git a/tests/data/precalculated_ref/model_binary.json b/tests/data/precalculated_ref/model_binary.json
new file mode 100644
index 00000000..ad51575d
--- /dev/null
+++ b/tests/data/precalculated_ref/model_binary.json
@@ -0,0 +1,26 @@
+{
+    "coefficients": {
+        "Ne": 0.1,
+        "Xe": 0.2,
+        "Ne-Ne": [0.31920357768344226, 0.11408975948272272, 0.029068754695561395, 0.0023246174838348764, -0.0022337475167244596, -0.0026790931934311678, -0.0021221466491732426, -0.0018267914463140644, -0.0012999752685888132, -0.00090056026904286157, -0.00073026003625551437, -0.00035527329773823118, -0.00040925958062680085, -4.0181715609995442e-05, -0.00020831461284810665, 0, 0, 0],
+        "Ne-Xe": [0.8217033119290309, 0.27557811098166918, 0.083184586469205846, 0.014313578022014472, -0.0012294822300832316, -0.0042642098393096415, -0.0040688165698538007, -0.0031992215555863178, -0.0025266724695686715, -0.0018574655558040636, -0.0011878606928438847, -0.00088196490621543576, -0.00049865989488555339, -0.00022921808360627403, -0.00021724195178396223, 0, 0, 0],
+        "Xe-Xe": [0.59380272944790102, 0.4396004824256215, 0.13120726288336987, 0.016012544230524194, -0.0095991013414302353, -0.013025719495121168, -0.010773164272733993, -0.0084114343547100796, -0.0057845878544200077, -0.0043834167985211435, -0.0027220390738168965, -0.0018171054284458299, -0.00099991762554714741, -0.000621098504991941, -0.00036692776508893954, 0, 0, 0]
+    },
+    "knots": {
+        "Ne-Ne": [2, 2, 2, 2, 2.2666666666666666, 2.5333333333333332, 2.7999999999999998, 3.0666666666666664, 3.333333333333333, 3.6000000000000001, 3.8666666666666667, 4.1333333333333329, 4.4000000000000004, 4.6666666666666661, 4.9333333333333336, 5.2000000000000002, 5.4666666666666668, 5.7333333333333334, 6, 6, 6, 6],
+        "Ne-Xe": [2.5, 2.5, 2.5, 2.5, 2.7999999999999998, 3.1000000000000001, 3.3999999999999999, 3.7000000000000002, 4, 4.2999999999999998, 4.5999999999999996, 4.9000000000000004, 5.1999999999999993, 5.5, 5.7999999999999998, 6.0999999999999996, 6.4000000000000004, 6.7000000000000002, 7, 7, 7, 7],
+        "Xe-Xe": [3, 3, 3, 3, 3.3333333333333335, 3.6666666666666665, 4, 4.333333333333333, 4.6666666666666661, 5, 5.333333333333333, 5.6666666666666661, 6, 6.333333333333333, 6.6666666666666661, 7, 7.333333333333333, 7.6666666666666661, 8, 8, 8, 8]
+    },
+    "data_coverage": [false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false],
+    "knot_strategy": "linear",
+    "offset_1b": false,
+    "leading_trim": 0,
+    "trailing_trim": 3,
+    "knots_map": {
+        "Ne-Ne": [2, 2, 2, 2, 2.2666666666666666, 2.5333333333333332, 2.7999999999999998, 3.0666666666666664, 3.333333333333333, 3.6000000000000001, 3.8666666666666667, 4.1333333333333329, 4.4000000000000004, 4.6666666666666661, 4.9333333333333336, 5.2000000000000002, 5.4666666666666668, 5.7333333333333334, 6, 6, 6, 6],
+        "Ne-Xe": [2.5, 2.5, 2.5, 2.5, 2.7999999999999998, 3.1000000000000001, 3.3999999999999999, 3.7000000000000002, 4, 4.2999999999999998, 4.5999999999999996, 4.9000000000000004, 5.1999999999999993, 5.5, 5.7999999999999998, 6.0999999999999996, 6.4000000000000004, 6.7000000000000002, 7, 7, 7, 7],
+        "Xe-Xe": [3, 3, 3, 3, 3.3333333333333335, 3.6666666666666665, 4, 4.333333333333333, 4.6666666666666661, 5, 5.333333333333333, 5.6666666666666661, 6, 6.333333333333333, 6.6666666666666661, 7, 7.333333333333333, 7.6666666666666661, 8, 8, 8, 8]
+    },
+    "element_list": ["Ne", "Xe"],
+    "degree": 2
+}
\ No newline at end of file
diff --git a/tests/data/precalculated_ref/model_unary.json b/tests/data/precalculated_ref/model_unary.json
new file mode 100644
index 00000000..a2e55db0
--- /dev/null
+++ b/tests/data/precalculated_ref/model_unary.json
@@ -0,0 +1,130 @@
+{
+    "coefficients": {
+        "W": -6.5637083648909931,
+        "W-W": [38.408889568988229, 38.408889568988229, 27.730916275207534, 18.75495710798414, 11.481012067318044, 5.4750576640813478, 1.7975987954601431, -0.46430584542571274, -0.47719436247997343, -0.32368482399979892, -0.21453513564549828, -0.13264136789689956, -0.043116575617924209, -0.050937711632254329, -0.033227268058668283, 0, 0, 0],
+        "W-W-W": [
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, -0.011511397229116009, -1.3776836861498465, -2.6199126178394163, -0.28966714170466229, 6.871077709690703e-05, 0.00018328292198380167, 0.00015279529064961508, 6.4472757164017906e-06, 0, 0, 0, 0, 0],
+                [0, -0.00027213139092673461, 0.0048239539061202671, 0.71223173843830878, 0.51107926008375359, 2.7453251307500617, 3.0589064872778806, -0.97743362961421765, 0.61649051802200949, 0.064873681332149352, -5.6219475422564372e-05, 0, 0, 0, 0],
+                [0, -0.093219036758595344, -1.5292767451567404, 5.0461529566837324, 2.8235080692979171, 2.4857838717149314, 1.0636844813579098, 1.2240789707530189, 3.9545316316258616, -0.29452239497099308, 0.03654357592213197, 2.1249046180794788e-09, 0, 0, 0],
+                [0, -0.19957617420716223, -3.7081805252096611, 0.10856077545288305, -0.91047618518785534, -2.3494979300370105, 0.96717362537398022, -2.2888638082451198, -1.132159213457437, 4.6931838009200453, -0.36467827620919607, -0.014012142342237622, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, -0.011511397229116009, -1.3776836861498465, -2.6199126178394163, -0.28966714170466229, 6.871077709690703e-05, 0.00018328292198380167, 0.00015279529064961508, 6.4472757164017906e-06, 0, 0, 0, 0, 0],
+                [0, -0.0014869313343161483, 0.2579182589652122, 1.0050316372129124, 1.6252804378094907, 4.6121515386491811, 1.5638053728517474, 2.1267286221961648, 1.4820306116633548, 0.4176523472135853, 4.717837209218048e-06, 0, 0, 0, 0],
+                [5.1327328421621044e-07, 0.25497909950437081, -0.065179794986717471, -3.9278068136992004, 2.0542729125343069, 0.64712618252517817, -1.2431069600706908, -1.0341076422671571, 1.7500966759086747, -9.4432039388249329, -0.03607656041869528, 1.4198328730833178e-06, 0, 0, 0],
+                [7.1059660020090907e-05, 0.81173843575531546, -2.0909447928695366, -2.9578007345433219, 0.82242529091251526, 0.3973921898111184, 0.4852309776544283, 0.51008394711271543, -0.65312421465232084, -0.10835588638926794, 1.6674596184825758, -0.33678006757103751, 0, 0, 0],
+                [0.0001431072503015647, -1.1112830966714684, 1.4777787235535012, 1.3964195885579629, 0.12576488441404474, 0.52750428102235936, 0.21563526836993765, 0.097139370993837737, 0.53918293970680442, -0.42479160078382838, 5.2517334348100775, 6.4398606000589265, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, -0.00027213139092673461, 0.0048239539061202671, 0.71223173843830878, 0.51107926008375359, 2.7453251307500617, 3.0589064872778806, -0.97743362961421765, 0.61649051802200949, 0.064873681332149352, -5.6219475422564372e-05, 0, 0, 0, 0],
+                [5.1327328421621044e-07, 0.25497909950437081, -0.065179794986717471, -3.9278068136992004, 2.0542729125343069, 0.64712618252517817, -1.2431069600706908, -1.0341076422671571, 1.7500966759086747, -9.4432039388249329, -0.03607656041869528, 1.4198328730833178e-06, 0, 0, 0],
+                [-0.0010001155018055559, 0.94456447982572633, 1.7474474619557037, -0.30309560136032715, -0.79065001946541436, 0.95166124810931596, 0.64574806853798394, 1.0709906356857379, 1.083867972911525, -2.3427576705453999, -29.726862664939819, -0.52173671754478479, 0, 0, 0],
+                [-0.17048247259942262, 7.843330430480929, 2.8458574719564096, -1.6456909208832673, -0.56281849308173504, 0.47453122340737153, 0.076434458508544884, 0.098510951341364117, 0.88041914640469987, 0.22815360626346226, -3.3953614782805306, -7.2090313728706663, 0, 0, 0],
+                [-0.34559987888042748, -1.110604227822267, -0.96805584046189297, 1.7162954156543029, 0.16135808146316388, 0.16025044436083738, 0.10157180208637395, -0.041243141369840405, 0.099122757430011074, 0.33465564907642037, 0.36527235431064464, -1.9482724791046273, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, -0.093219036758595344, -1.5292767451567404, 5.0461529566837324, 2.8235080692979171, 2.4857838717149314, 1.0636844813579098, 1.2240789707530189, 3.9545316316258616, -0.29452239497099308, 0.03654357592213197, 2.1249046180794788e-09, 0, 0, 0],
+                [7.1059660020090907e-05, 0.81173843575531546, -2.0909447928695366, -2.9578007345433219, 0.82242529091251526, 0.3973921898111184, 0.4852309776544283, 0.51008394711271543, -0.65312421465232084, -0.10835588638926794, 1.6674596184825758, -0.33678006757103751, 0, 0, 0],
+                [-0.17048247259942262, 7.843330430480929, 2.8458574719564096, -1.6456909208832673, -0.56281849308173504, 0.47453122340737153, 0.076434458508544884, 0.098510951341364117, 0.88041914640469987, 0.22815360626346226, -3.3953614782805306, -7.2090313728706663, 0, 0, 0],
+                [-0.88427340093964535, 1.6777819013465154, 5.8460703159591922, 0.2018784989440601, -0.078356071666925015, 0.15924505168775516, 0.049525650853021706, -0.041467283879863766, 0.12562450782382817, 0.42684832529485212, -0.44877814155079704, 0.77851118148099963, 0, 0, 0],
+                [-2.0862842648259279, -1.8540386594142206, -1.0719910116807712, 0.13939692696932646, 0.031913035293120499, 0.070133438171547505, 0.11928421291561837, -0.020640371872463328, 0.058305822783446343, 0.059767540611448865, 0.068054218189572371, -0.081348931724700424, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, -0.19957617420716223, -3.7081805252096611, 0.10856077545288305, -0.91047618518785534, -2.3494979300370105, 0.96717362537398022, -2.2888638082451198, -1.132159213457437, 4.6931838009200453, -0.36467827620919607, -0.014012142342237622, 0, 0, 0],
+                [0.0001431072503015647, -1.1112830966714684, 1.4777787235535012, 1.3964195885579629, 0.12576488441404474, 0.52750428102235936, 0.21563526836993765, 0.097139370993837737, 0.53918293970680442, -0.42479160078382838, 5.2517334348100775, 6.4398606000589265, 0, 0, 0],
+                [-0.34559987888042748, -1.110604227822267, -0.96805584046189297, 1.7162954156543029, 0.16135808146316388, 0.16025044436083738, 0.10157180208637395, -0.041243141369840405, 0.099122757430011074, 0.33465564907642037, 0.36527235431064464, -1.9482724791046273, 0, 0, 0],
+                [-2.0862842648259279, -1.8540386594142206, -1.0719910116807712, 0.13939692696932646, 0.031913035293120499, 0.070133438171547505, 0.11928421291561837, -0.020640371872463328, 0.058305822783446343, 0.059767540611448865, 0.068054218189572371, -0.081348931724700424, 0, 0, 0],
+                [-0.54247884278448155, 1.2703949794560363, -1.2127840281667461, 0.0055939005821572813, 0.016700905370142551, -0.03389824047602847, 0.061761787136710093, -0.010488973395504489, -0.011662718955234703, 0.024825748739471232, -0.038141662894715066, 0.013655219275751107, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ],
+            [
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            ]
+        ]
+    },
+    "knots": {
+        "W-W": [0.001, 0.001, 0.001, 0.001, 0.36759999999999998, 0.73419999999999996, 1.1008, 1.4674, 1.8340000000000001, 2.2006000000000001, 2.5672000000000001, 2.9338000000000002, 3.3003999999999998, 3.6669999999999998, 4.0335999999999999, 4.4001999999999999, 4.7667999999999999, 5.1334, 5.5, 5.5, 5.5, 5.5],
+        "W-W-W": [
+            [1.5, 1.5, 1.5, 1.5, 1.8333333332999999, 2.1666666666999999, 2.5, 2.8333333333000001, 3.1666666666999999, 3.5, 3.5, 3.5, 3.5],
+            [1.5, 1.5, 1.5, 1.5, 1.8333333332999999, 2.1666666666999999, 2.5, 2.8333333333000001, 3.1666666666999999, 3.5, 3.5, 3.5, 3.5],
+            [1.5, 1.5, 1.5, 1.5, 1.9583333332999999, 2.4166666666999999, 2.875, 3.3333333333000001, 3.7916666666999999, 4.25, 4.7083333332999997, 5.1666666667000003, 5.625, 6.0833333332999997, 6.5416666667000003, 7, 7, 7, 7]
+        ]
+    },
+    "data_coverage": [true, false, false, false, false, false, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true],
+    "knot_strategy": "linear",
+    "offset_1b": true,
+    "leading_trim": 0,
+    "trailing_trim": 3,
+    "knots_map": {
+        "W-W": [0.001, 0.001, 0.001, 0.001, 0.36759999999999998, 0.73419999999999996, 1.1008, 1.4674, 1.8340000000000001, 2.2006000000000001, 2.5672000000000001, 2.9338000000000002, 3.3003999999999998, 3.6669999999999998, 4.0335999999999999, 4.4001999999999999, 4.7667999999999999, 5.1334, 5.5, 5.5, 5.5, 5.5],
+        "W-W-W": [
+            [1.5, 1.5, 1.5, 1.5, 1.8333333332999999, 2.1666666666999999, 2.5, 2.8333333333000001, 3.1666666666999999, 3.5, 3.5, 3.5, 3.5],
+            [1.5, 1.5, 1.5, 1.5, 1.8333333332999999, 2.1666666666999999, 2.5, 2.8333333333000001, 3.1666666666999999, 3.5, 3.5, 3.5, 3.5],
+            [1.5, 1.5, 1.5, 1.5, 1.9583333332999999, 2.4166666666999999, 2.875, 3.3333333333000001, 3.7916666666999999, 4.25, 4.7083333332999997, 5.1666666667000003, 5.625, 6.0833333332999997, 6.5416666667000003, 7, 7, 7, 7]
+        ]
+    },
+    "element_list": ["W"],
+    "degree": 3
+}
\ No newline at end of file
diff --git a/tests/test_calculator.py b/tests/test_calculator.py
index 2360ff46..c22e06c9 100644
--- a/tests/test_calculator.py
+++ b/tests/test_calculator.py
@@ -1,5 +1,7 @@
 import numpy as np
 import ase
+import os
+import uf3
 from uf3.data import composition
 from uf3.representation import bspline
 from uf3.regression import least_squares
@@ -7,7 +9,7 @@
 
 
 class TestCalculator:
-    def test_unary(self):
+    def test_unary_dimer(self):
         element_list = ['W']
         chemical_system = composition.ChemicalSystem(element_list)
         bspline_config = bspline.BSplineBasis(chemical_system,
@@ -46,3 +48,68 @@ def test_unary(self):
         assert np.isclose(geom.get_potential_energy(), -15.33335)
         forces = geom.get_forces()
         assert np.allclose(forces, [[0, -17.3656864, 0], [0, 17.3656864, 0]])
+
+    def test_unary_trimer(self):
+        geom = ase.Atoms("W3",
+                         positions=[[0, 0, 0], [2, 0, 0], [0, 3, 0]],
+                         pbc=False,
+                         cell=None)
+        pkg_directory = os.path.dirname(os.path.dirname(uf3.__file__))
+        data_directory = os.path.join(pkg_directory, "tests/data")
+        model_file = os.path.join(data_directory, "precalculated_ref",
+                                  "model_unary.json")
+        model = least_squares.WeightedLinearModel.from_json(model_file)
+        calc = calculator.UFCalculator(model)
+        geom.calc = calc
+        energy = geom.get_potential_energy()
+        assert np.isclose(energy, -18.79979353611411)
+        forces = geom.get_forces()
+        assert np.allclose(forces, [[-12.26367499,   0.15140673,   0.        ],
+                                    [ 12.05608935,   0.31137845,   0.        ],
+                                    [  0.20758563,  -0.46278518,   0.        ]]
+                           )
+
+    def test_unary_pbc(self):
+        geom = ase.Atoms("W8",
+                         positions=[[ 0.00,  0.00,  0.00], [ 2.89,  0.12, -0.04],
+                                    [-0.32,  2.71, -0.11], [ 2.65,  2.81,  0.37],
+                                    [ 0.00,  0.00,  3.00], [ 2.64,  0.00,  3.00],
+                                    [-0.08,  2.94,  3.16], [ 2.53,  2.87,  3.23]],
+                         pbc=True,
+                         cell=np.eye(3)*2.74*2)
+        pkg_directory = os.path.dirname(os.path.dirname(uf3.__file__))
+        data_directory = os.path.join(pkg_directory, "tests/data")
+        model_file = os.path.join(data_directory, "precalculated_ref",
+                                  "model_unary.json")
+        model = least_squares.WeightedLinearModel.from_json(model_file)
+        calc = calculator.UFCalculator(model)
+        geom.calc = calc
+        energy = geom.get_potential_energy()
+        assert np.isclose(energy, -76.358888229785)
+        forces = geom.get_forces()
+        assert np.allclose(forces, [[ 1.36696442, -0.46307   ,  1.78573347],
+                                    [ 0.20112587,  0.17014795,  1.22172728],
+                                    [-0.66043959, -1.08374173,  6.78845939],
+                                    [-1.30913745,  0.36888897,  1.48182124],
+                                    [-0.33315563,  1.28359885, -1.56572912],
+                                    [ 0.01504262,  0.06574851, -2.38044283],
+                                    [ 0.25436762,  0.2491558 , -7.48063062],
+                                    [ 0.46523214, -0.59072835,  0.14906119]]
+                           )
+
+    def test_binary(self):
+        geom = ase.Atoms("NeXe", positions=[[0, 0, 0], [3.1, 0, 0]], pbc=False)
+        pkg_directory = os.path.dirname(os.path.dirname(uf3.__file__))
+        data_directory = os.path.join(pkg_directory, "tests/data")
+        model_file = os.path.join(data_directory, "precalculated_ref",
+                                  "model_binary.json")
+        model = least_squares.WeightedLinearModel.from_json(model_file)
+        calc = calculator.UFCalculator(model)
+        geom.calc = calc
+        energy = geom.get_potential_energy()
+        assert np.isclose(energy, 0.3464031387757268)
+        forces = geom.get_forces()
+        assert np.allclose(forces, [[-0.28138023,  0.        ,  0.        ],
+                                    [ 0.28138023,  0.        ,  0.        ]]
+                           )
+    
\ No newline at end of file