@mat3ra/made 2025.1.1-0 → 2025.1.18-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2025.1.1-0",
3
+ "version": "2025.1.18-0",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
@@ -12,6 +12,16 @@ DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"}
12
12
  DEFAULT_TYPE = "TRI"
13
13
 
14
14
 
15
+ class LatticeVectors(BaseModel):
16
+ """
17
+ A class to represent the lattice vectors.
18
+ """
19
+
20
+ a: List[float] = [1.0, 0.0, 0.0]
21
+ b: List[float] = [0.0, 1.0, 0.0]
22
+ c: List[float] = [0.0, 0.0, 1.0]
23
+
24
+
15
25
  class Lattice(RoundNumericValuesMixin, BaseModel):
16
26
  a: float = 1.0
17
27
  b: float = a
@@ -23,7 +33,7 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
23
33
  type: str = DEFAULT_TYPE
24
34
 
25
35
  @property
26
- def vectors(self) -> List[List[float]]:
36
+ def vectors(self) -> LatticeVectors:
27
37
  a = self.a
28
38
  b = self.b
29
39
  c = self.c
@@ -44,12 +54,12 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
44
54
  cos_gamma_star = math.cos(gamma_star)
45
55
  sin_gamma_star = math.sin(gamma_star)
46
56
 
47
- # Return the lattice matrix using the derived trigonometric values
48
- return [
49
- [a * sin_beta, 0.0, a * cos_beta],
50
- [-b * sin_alpha * cos_gamma_star, b * sin_alpha * sin_gamma_star, b * cos_alpha],
51
- [0.0, 0.0, c],
52
- ]
57
+ # Calculate the vectors
58
+ vector_a = [a * sin_beta, 0.0, a * cos_beta]
59
+ vector_b = [-b * sin_alpha * cos_gamma_star, b * sin_alpha * sin_gamma_star, b * cos_alpha]
60
+ vector_c = [0.0, 0.0, c]
61
+
62
+ return LatticeVectors(a=vector_a, b=vector_b, c=vector_c)
53
63
 
54
64
  @classmethod
55
65
  def from_vectors_array(
@@ -96,7 +106,9 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
96
106
 
97
107
  @property
98
108
  def vector_arrays(self) -> List[List[float]]:
99
- return self.vectors
109
+ """Returns lattice vectors as a nested array"""
110
+ v = self.vectors
111
+ return [v.a, v.b, v.c]
100
112
 
101
113
  @property
102
114
  def cell(self) -> Cell:
@@ -16,10 +16,7 @@ from .enums import PointDefectTypeEnum
16
16
  from .factories import DefectBuilderFactory
17
17
 
18
18
 
19
- def create_defect(
20
- configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
21
- builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
22
- ) -> Material:
19
+ def get_material_with_defect(configuration, builder_parameters):
23
20
  """
24
21
  Return a material with a selected defect added.
25
22
 
@@ -38,6 +35,23 @@ def create_defect(
38
35
  return builder.get_material(configuration) if builder else configuration.crystal
39
36
 
40
37
 
38
+ def create_defect(
39
+ configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
40
+ builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
41
+ ) -> Material:
42
+ """
43
+ Return a material with a selected defect added.
44
+
45
+ Args:
46
+ configuration: The configuration of the defect to be added.
47
+ builder_parameters: The parameters to be used by the defect builder.
48
+
49
+ Returns:
50
+ The material with the defect added.
51
+ """
52
+ return get_material_with_defect(configuration, builder_parameters)
53
+
54
+
41
55
  def create_defects(
42
56
  configurations: Union[List[PointDefectConfiguration], List[AdatomSlabPointDefectConfiguration]],
43
57
  builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
@@ -57,10 +71,7 @@ def create_defects(
57
71
  for configuration in configurations:
58
72
  if material_with_defect:
59
73
  configuration.crystal = material_with_defect
60
-
61
- BuilderClass = DefectBuilderFactory.get_class_by_name(configuration.defect_type)
62
- builder = BuilderClass(builder_parameters)
63
- material_with_defect = builder.get_material(configuration) if builder else configuration.crystal
74
+ material_with_defect = get_material_with_defect(configuration, builder_parameters)
64
75
 
65
76
  return material_with_defect
66
77
 
@@ -146,6 +146,7 @@ class VoronoiInterstitialPointDefectBuilder(PointDefectBuilder):
146
146
  ),
147
147
  )
148
148
  pymatgen_structure.append(closest_interstitial.site.species, closest_interstitial.site.frac_coords)
149
+ pymatgen_structure.remove_oxidation_states()
149
150
  return [pymatgen_structure]
150
151
 
151
152
 
@@ -73,11 +73,11 @@ class NanoribbonBuilder(BaseBuilder):
73
73
  nanoribbon_length, nanoribbon_width = nanoribbon_width, nanoribbon_length
74
74
  vacuum_width, vacuum_length = vacuum_length, vacuum_width
75
75
 
76
- length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors[0]), np.array([1, 0, 0]))
77
- width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors[1]), np.array([0, 1, 0]))
78
- height_cartesian = np.dot(np.array(material.lattice.vectors[2]), np.array([0, 0, 1]))
79
- vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors[0]), np.array([1, 0, 0]))
80
- vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors[1]), np.array([0, 1, 0]))
76
+ length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0]))
77
+ width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0]))
78
+ height_cartesian = np.dot(np.array(material.lattice.vectors.c), np.array([0, 0, 1]))
79
+ vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0]))
80
+ vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0]))
81
81
 
82
82
  return length_cartesian, width_cartesian, height_cartesian, vacuum_length_cartesian, vacuum_width_cartesian
83
83
 
@@ -1,4 +1,5 @@
1
- from typing import Union, List
1
+ from collections import Counter
2
+ from typing import Union, List, Dict
2
3
 
3
4
  from mat3ra.made.material import Material
4
5
  from .configuration import PassivationConfiguration
@@ -36,3 +37,24 @@ def get_unique_coordination_numbers(
36
37
  material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab)
37
38
  material_with_crystal_sites.analyze()
38
39
  return material_with_crystal_sites.get_unique_coordination_numbers(cutoff=cutoff)
40
+
41
+
42
+ def get_coordination_numbers_distribution(
43
+ configuration: PassivationConfiguration,
44
+ cutoff: float = 3.0,
45
+ ) -> Dict[int, int]:
46
+ """
47
+ Get the unique coordination numbers for the provided passivation configuration and cutoff radius.
48
+
49
+ Args:
50
+ configuration (PassivationConfiguration): The configuration object.
51
+ cutoff (float): The cutoff radius for defining neighbors.
52
+ Returns:
53
+ set: The unique coordination numbers.
54
+ """
55
+ material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab)
56
+ material_with_crystal_sites.analyze()
57
+ coordinatation_numbers = material_with_crystal_sites.get_coordination_numbers(cutoff=cutoff)
58
+ sorted_coordinatation_numbers = sorted(coordinatation_numbers.values)
59
+ distribution = dict(Counter(sorted_coordinatation_numbers))
60
+ return distribution
@@ -65,7 +65,7 @@ class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder):
65
65
 
66
66
  class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder):
67
67
  def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]:
68
- cell_vectors = configuration.material.lattice.vectors
68
+ cell_vectors = configuration.material.lattice.vector_arrays
69
69
  return [configuration.perturbation_function_holder.transform_coordinates(coord) for coord in cell_vectors]
70
70
 
71
71
  def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material:
@@ -40,7 +40,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
40
40
 
41
41
 
42
42
  def translate_to_z_level(
43
- material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom"
43
+ material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6
44
44
  ) -> Material:
45
45
  """
46
46
  Translate atoms to the specified z-level.
@@ -48,15 +48,16 @@ def translate_to_z_level(
48
48
  Args:
49
49
  material (Material): The material object to normalize.
50
50
  z_level (str): The z-level to translate the atoms to (top, bottom, center)
51
+ tolerance (float): The tolerance value to avoid moving past unit cell.
51
52
  Returns:
52
53
  Material: The translated material object.
53
54
  """
54
55
  min_z = get_atomic_coordinates_extremum(material, "min")
55
56
  max_z = get_atomic_coordinates_extremum(material)
56
57
  if z_level == "top":
57
- material = translate_by_vector(material, vector=[0, 0, 1 - max_z])
58
+ material = translate_by_vector(material, vector=[0, 0, 1 - max_z - tolerance])
58
59
  elif z_level == "bottom":
59
- material = translate_by_vector(material, vector=[0, 0, -min_z])
60
+ material = translate_by_vector(material, vector=[0, 0, -min_z + tolerance])
60
61
  elif z_level == "center":
61
62
  material = translate_by_vector(material, vector=[0, 0, (1 - min_z - max_z) / 2])
62
63
  return material
@@ -201,7 +202,7 @@ def filter_by_layers(
201
202
  if central_atom_id is not None:
202
203
  center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id)
203
204
  vectors = material.lattice.vectors
204
- direction_vector = vectors[2]
205
+ direction_vector = vectors.c
205
206
 
206
207
  def condition(coordinate):
207
208
  return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness)
@@ -240,6 +241,7 @@ def filter_by_sphere(
240
241
  center_coordinate: List[float] = [0, 0, 0],
241
242
  central_atom_id: Optional[int] = None,
242
243
  radius: float = 1,
244
+ tolerance: float = 0.0,
243
245
  invert: bool = False,
244
246
  ) -> Material:
245
247
  """
@@ -249,6 +251,7 @@ def filter_by_sphere(
249
251
  material (Material): The material object to filter.
250
252
  central_atom_id (int): Index of the central atom.
251
253
  radius (float): Radius of the sphere in angstroms.
254
+ tolerance (float): The tolerance value to include atoms on the edge of the sphere.
252
255
  invert (bool): Whether to invert the selection.
253
256
 
254
257
  Returns:
@@ -258,7 +261,7 @@ def filter_by_sphere(
258
261
  material=material,
259
262
  atom_index=central_atom_id,
260
263
  coordinate=center_coordinate,
261
- radius=radius,
264
+ radius=radius + tolerance,
262
265
  )
263
266
  return filter_by_ids(material, ids, invert=invert)
264
267
 
@@ -268,6 +271,7 @@ def filter_by_circle_projection(
268
271
  x: float = 0.5,
269
272
  y: float = 0.5,
270
273
  r: float = 0.25,
274
+ tolerance: float = 0.0,
271
275
  use_cartesian_coordinates: bool = False,
272
276
  invert_selection: bool = False,
273
277
  ) -> Material:
@@ -279,6 +283,7 @@ def filter_by_circle_projection(
279
283
  x (float): The x-coordinate of the circle center.
280
284
  y (float): The y-coordinate of the circle center.
281
285
  r (float): The radius of the circle.
286
+ tolerance (float): The tolerance value to include atoms on the edge of the circle.
282
287
  use_cartesian_coordinates (bool): Whether to use cartesian coordinates
283
288
  invert_selection (bool): Whether to invert the selection.
284
289
 
@@ -287,7 +292,7 @@ def filter_by_circle_projection(
287
292
  """
288
293
 
289
294
  def condition(coordinate):
290
- return is_coordinate_in_cylinder(coordinate, [x, y, 0], r, min_z=0, max_z=1)
295
+ return is_coordinate_in_cylinder(coordinate, [x, y, 0], r + tolerance, min_z=0, max_z=1)
291
296
 
292
297
  return filter_by_condition_on_coordinates(
293
298
  material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
@@ -300,6 +305,7 @@ def filter_by_cylinder(
300
305
  min_z: Optional[float] = None,
301
306
  max_z: Optional[float] = None,
302
307
  radius: float = 0.25,
308
+ tolerance: float = 0.0,
303
309
  use_cartesian_coordinates: bool = False,
304
310
  invert_selection: bool = False,
305
311
  ) -> Material:
@@ -331,7 +337,9 @@ def filter_by_cylinder(
331
337
  )
332
338
 
333
339
  def condition(coordinate):
334
- return is_coordinate_in_cylinder(coordinate, center_position, radius, min_z, max_z)
340
+ return is_coordinate_in_cylinder(
341
+ coordinate, center_position, radius + tolerance, min_z - tolerance, max_z + tolerance
342
+ )
335
343
 
336
344
  return filter_by_condition_on_coordinates(
337
345
  material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
@@ -342,6 +350,7 @@ def filter_by_rectangle_projection(
342
350
  material: Material,
343
351
  min_coordinate: List[float] = [0, 0],
344
352
  max_coordinate: List[float] = [1, 1],
353
+ tolerance: float = 0.0,
345
354
  use_cartesian_coordinates: bool = False,
346
355
  invert_selection: bool = False,
347
356
  ) -> Material:
@@ -352,6 +361,7 @@ def filter_by_rectangle_projection(
352
361
  material (Material): The material object to filter.
353
362
  min_coordinate (List[float]): The minimum coordinate of the rectangle.
354
363
  max_coordinate (List[float]): The maximum coordinate of the rectangle.
364
+ tolerance (float): The tolerance value to include atoms on the edges of the rectangle.
355
365
  use_cartesian_coordinates (bool): Whether to use cartesian coordinates
356
366
  invert_selection (bool): Whether to invert the selection.
357
367
 
@@ -360,8 +370,8 @@ def filter_by_rectangle_projection(
360
370
  """
361
371
  min_z = get_atomic_coordinates_extremum(material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates)
362
372
  max_z = get_atomic_coordinates_extremum(material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates)
363
- min_coordinate = min_coordinate[:2] + [min_z]
364
- max_coordinate = max_coordinate[:2] + [max_z]
373
+ min_coordinate = [c - tolerance for c in min_coordinate[:2]] + [min_z]
374
+ max_coordinate = [c + tolerance for c in max_coordinate[:2]] + [max_z]
365
375
 
366
376
  def condition(coordinate):
367
377
  return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
@@ -375,6 +385,7 @@ def filter_by_box(
375
385
  material: Material,
376
386
  min_coordinate: Optional[List[float]] = None,
377
387
  max_coordinate: Optional[List[float]] = None,
388
+ tolerance: float = 0.0,
378
389
  use_cartesian_coordinates: bool = False,
379
390
  invert_selection: bool = False,
380
391
  ) -> Material:
@@ -385,6 +396,7 @@ def filter_by_box(
385
396
  material (Material): The material to filter.
386
397
  min_coordinate (List[float], optional): The minimum coordinate of the box. Defaults to material's min.
387
398
  max_coordinate (List[float], optional): The maximum coordinate of the box. Defaults to material's max.
399
+ tolerance (float): The tolerance value to include atoms on the edges of the box.
388
400
  use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
389
401
  invert_selection (bool): Whether to invert the selection.
390
402
 
@@ -396,6 +408,9 @@ def filter_by_box(
396
408
  min_coordinate = min_coordinate if min_coordinate is not None else default_min
397
409
  max_coordinate = max_coordinate if max_coordinate is not None else default_max
398
410
 
411
+ min_coordinate = [c - tolerance for c in min_coordinate]
412
+ max_coordinate = [c + tolerance for c in max_coordinate]
413
+
399
414
  def condition(coordinate):
400
415
  return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
401
416
 
@@ -411,6 +426,7 @@ def filter_by_triangle_projection(
411
426
  coordinate_3: Optional[List[float]] = None,
412
427
  min_z: Optional[float] = None,
413
428
  max_z: Optional[float] = None,
429
+ tolerance: float = 0.0,
414
430
  use_cartesian_coordinates: bool = False,
415
431
  invert_selection: bool = False,
416
432
  ) -> Material:
@@ -424,6 +440,7 @@ def filter_by_triangle_projection(
424
440
  coordinate_3 (List[float], optional): Third vertex of the triangle. Defaults to material's corner.
425
441
  min_z (float, optional): Lower z-limit. Defaults to material's min z.
426
442
  max_z (float, optional): Upper z-limit. Defaults to material's max z.
443
+ tolerance (float): The tolerance value to include atoms on the top and bottom faces of the prism.
427
444
  use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
428
445
  invert_selection (bool): Whether to invert the selection.
429
446
 
@@ -440,13 +457,25 @@ def filter_by_triangle_projection(
440
457
  min_z = get_atomic_coordinates_extremum(
441
458
  material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates
442
459
  )
460
+ else:
461
+ min_z -= tolerance
462
+
443
463
  if max_z is None:
444
464
  max_z = get_atomic_coordinates_extremum(
445
465
  material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates
446
466
  )
467
+ else:
468
+ max_z += tolerance
447
469
 
448
470
  def condition(coordinate):
449
- return is_coordinate_in_triangular_prism(coordinate, coordinate_1, coordinate_2, coordinate_3, min_z, max_z)
471
+ return is_coordinate_in_triangular_prism(
472
+ coordinate,
473
+ coordinate_1,
474
+ coordinate_2,
475
+ coordinate_3,
476
+ min_z,
477
+ max_z,
478
+ )
450
479
 
451
480
  return filter_by_condition_on_coordinates(
452
481
  material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
@@ -535,7 +564,7 @@ def remove_vacuum(material: Material, from_top=True, from_bottom=True, fixed_pad
535
564
  new_basis.to_cartesian()
536
565
  new_lattice = translated_material.lattice
537
566
  new_lattice.c = get_atomic_coordinates_extremum(translated_material, use_cartesian_coordinates=True) + fixed_padding
538
- new_basis.cell.vector3 = new_lattice.vectors[2]
567
+ new_basis.cell.vector3 = new_lattice.vectors.c
539
568
  new_basis.to_crystal()
540
569
  new_material = material.clone()
541
570
 
@@ -10,24 +10,6 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3])
10
10
  DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR
11
11
 
12
12
 
13
- # TODO: convert to accept ASE Atoms object
14
- def translate_to_bottom_pymatgen_structure(structure: PymatgenStructure):
15
- """
16
- Translate the structure to the bottom of the cell.
17
- Args:
18
- structure (PymatgenStructure): The pymatgen Structure object to translate.
19
-
20
- Returns:
21
- PymatgenStructure: The translated pymatgen Structure object.
22
- """
23
- min_c = min(site.c for site in structure)
24
- translation_vector = [0, 0, -min_c]
25
- translated_structure = structure.copy()
26
- for site in translated_structure:
27
- site.coords += translation_vector
28
- return translated_structure
29
-
30
-
31
13
  def decorator_convert_2x2_to_3x3(func: Callable) -> Callable:
32
14
  """
33
15
  Decorator to convert a 2x2 matrix to a 3x3 matrix.
@@ -0,0 +1,17 @@
1
+ from mat3ra.made.lattice import Lattice
2
+
3
+
4
+ def test_lattice_vectors_access():
5
+ lattice = Lattice(a=2.0, b=3.0, c=4.0)
6
+
7
+ # Test individual vector access
8
+ assert isinstance(lattice.vectors.a, list)
9
+ assert isinstance(lattice.vectors.b, list)
10
+ assert isinstance(lattice.vectors.c, list)
11
+
12
+ # Test vector arrays access
13
+ arrays = lattice.vector_arrays
14
+ assert len(arrays) == 3
15
+ assert arrays[0] == lattice.vectors.a
16
+ assert arrays[1] == lattice.vectors.b
17
+ assert arrays[2] == lattice.vectors.c
@@ -25,4 +25,15 @@ def test_basis_to_json():
25
25
  assertion_utils.assert_deep_almost_equal(expected_basis_config, basis.to_json())
26
26
 
27
27
 
28
- # TODO: Add test to check if basis.cell is changed when lattice of material is changed, and vice versa
28
+ def test_basis_cell_lattice_sync():
29
+ """Test synchronization between basis.cell and material.lattice"""
30
+ material = Material.create(Material.default_config)
31
+
32
+ # Change lattice vectors
33
+ new_vectors = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]]
34
+ material.set_new_lattice_vectors(*new_vectors)
35
+
36
+ # Verify basis.cell matches new lattice vectors
37
+ assertion_utils.assert_deep_almost_equal(new_vectors, material.basis.cell.vectors_as_array)
38
+
39
+ assertion_utils.assert_deep_almost_equal(new_vectors, material.lattice.vector_arrays)
@@ -64,7 +64,7 @@ def test_create_twisted_nanoribbon_interface():
64
64
 
65
65
  expected_cell_vectors = [[15.102811, 0.0, 0.0], [0.0, 16.108175208, 0.0], [0.0, 0.0, 20.0]]
66
66
  expected_coordinate = [0.704207885, 0.522108183, 0.65]
67
- assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.vectors)
67
+ assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.vector_arrays)
68
68
  assertion_utils.assert_deep_almost_equal(expected_coordinate, interface.basis.coordinates.values[42])
69
69
 
70
70
 
@@ -46,4 +46,4 @@ def test_distance_preserved_sine_perturbation():
46
46
  [-12.043583, 21.367367, 0.0],
47
47
  [0.0, 0.0, 20.0],
48
48
  ]
49
- assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.vectors)
49
+ assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.vector_arrays)
@@ -160,7 +160,7 @@ def test_remove_vacuum():
160
160
  material = Material(SI_SLAB)
161
161
  material_down = translate_to_z_level(material, z_level="bottom")
162
162
 
163
- assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json())
163
+ assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json(), atol=1e-3)
164
164
 
165
165
 
166
166
  def test_rotate():