@mat3ra/made 2025.11.24-0 → 2025.12.29-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.11.24-0",
3
+ "version": "2025.12.29-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",
package/pyproject.toml CHANGED
@@ -21,7 +21,6 @@ dependencies = [
21
21
  "mat3ra-utils",
22
22
  "mat3ra-esse",
23
23
  "mat3ra-code"
24
-
25
24
  ]
26
25
 
27
26
  [project.optional-dependencies]
@@ -31,6 +30,7 @@ tools = [
31
30
  "pymatgen==2024.4.13",
32
31
  "ase",
33
32
  "pymatgen-analysis-defects==2024.4.23",
33
+ "mat3ra-periodic-table>=2025.12.26",
34
34
  ]
35
35
  dev = [
36
36
  "pre-commit",
@@ -1,7 +1,15 @@
1
- from .functions import FunctionHolder, PerturbationFunctionHolder, SineWavePerturbationFunctionHolder
1
+ from .functions import (
2
+ AtomicMassDependentFunctionHolder,
3
+ FunctionHolder,
4
+ MaxwellBoltzmannDisplacementHolder,
5
+ PerturbationFunctionHolder,
6
+ SineWavePerturbationFunctionHolder,
7
+ )
2
8
 
3
9
  __all__ = [
10
+ "AtomicMassDependentFunctionHolder",
4
11
  "FunctionHolder",
12
+ "MaxwellBoltzmannDisplacementHolder",
5
13
  "PerturbationFunctionHolder",
6
14
  "SineWavePerturbationFunctionHolder",
7
15
  ]
@@ -1,9 +1,13 @@
1
+ from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder
1
2
  from .function_holder import FunctionHolder
3
+ from .maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder
2
4
  from .perturbation_function_holder import PerturbationFunctionHolder
3
5
  from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder
4
6
 
5
7
  __all__ = [
8
+ "AtomicMassDependentFunctionHolder",
6
9
  "FunctionHolder",
10
+ "MaxwellBoltzmannDisplacementHolder",
7
11
  "PerturbationFunctionHolder",
8
12
  "SineWavePerturbationFunctionHolder",
9
13
  ]
@@ -0,0 +1,32 @@
1
+ from typing import Any, List, Optional, Union
2
+
3
+ import sympy as sp
4
+ from mat3ra.periodic_table.helpers import get_atomic_mass_from_element
5
+
6
+ from .function_holder import FunctionHolder
7
+
8
+
9
+ class AtomicMassDependentFunctionHolder(FunctionHolder):
10
+ variables: List[str] = ["x", "y", "z", "m"]
11
+
12
+ def __init__(
13
+ self,
14
+ function: Union[sp.Expr, str] = sp.Symbol("f"),
15
+ variables: Optional[List[str]] = None,
16
+ **data: Any,
17
+ ):
18
+ if variables is None:
19
+ expr = self._to_expr(function)
20
+ vs = sorted(expr.free_symbols, key=lambda s: s.name)
21
+ variables = [str(v) for v in vs] or ["x", "y", "z", "m"]
22
+
23
+ super().__init__(function=function, variables=variables, **data)
24
+
25
+ @staticmethod
26
+ def get_atomic_mass(coordinate: List[float], material) -> float:
27
+ if material is None:
28
+ raise ValueError("Material is required to extract atomic mass")
29
+
30
+ atom_id = material.basis.coordinates.get_element_id_by_value(coordinate)
31
+ element = material.basis.elements.get_element_value_by_index(atom_id)
32
+ return get_atomic_mass_from_element(element)
@@ -52,7 +52,7 @@ class FunctionHolder(InMemoryEntityPydantic):
52
52
  def function_str(self) -> str:
53
53
  return str(self.function)
54
54
 
55
- def apply_function(self, coordinate: List[float]) -> float:
55
+ def apply_function(self, coordinate: List[float], **kwargs: Any) -> float:
56
56
  values = [coordinate[AXIS_TO_INDEX_MAP[var]] for var in self.variables]
57
57
  return self.function_numeric(*values)
58
58
 
@@ -0,0 +1,39 @@
1
+ from typing import Any, List, Optional
2
+
3
+ import numpy as np
4
+ from pydantic import Field, model_validator
5
+
6
+ from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder
7
+
8
+ DEFAULT_DISORDER_PARAMETER = 1.0
9
+
10
+
11
+ class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder):
12
+ disorder_parameter: float = Field(
13
+ default=DEFAULT_DISORDER_PARAMETER,
14
+ exclude=True,
15
+ description="Disorder parameter. Can be viewed as effective temperature in eV.",
16
+ )
17
+ random_seed: Optional[int] = Field(default=None, exclude=True)
18
+ random_state: Any = Field(default=None, exclude=True)
19
+ is_mass_used: bool = Field(default=True, exclude=True)
20
+
21
+ @model_validator(mode="after")
22
+ def setup_random_state(self):
23
+ if self.random_state is None:
24
+ self.random_state = np.random.RandomState(self.random_seed) if self.random_seed is not None else np.random
25
+ return self
26
+
27
+ def apply_function(self, coordinate, material=None) -> List[float]:
28
+ if material is None:
29
+ raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' kwargs")
30
+
31
+ if self.is_mass_used:
32
+ mass = self.get_atomic_mass(coordinate, material)
33
+ variance = self.disorder_parameter / mass
34
+ else:
35
+ variance = self.disorder_parameter
36
+
37
+ std_dev = np.sqrt(variance)
38
+ displacement = self.random_state.normal(0.0, std_dev, size=3)
39
+ return displacement.tolist()
@@ -1,13 +1,14 @@
1
- from typing import Union
1
+ from typing import Optional, Union
2
2
 
3
3
  import sympy as sp
4
- from mat3ra.made.material import Material
5
4
 
6
- from ..... import MaterialWithBuildMetadata
5
+ from mat3ra.made.material import Material
7
6
  from .builders.base import PerturbationBuilder
8
7
  from .builders.isometric import IsometricPerturbationBuilder
9
8
  from .configuration import PerturbationConfiguration
10
9
  from .functions import PerturbationFunctionHolder
10
+ from .functions.maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder
11
+ from ..... import MaterialWithBuildMetadata
11
12
 
12
13
 
13
14
  def create_perturbation(
@@ -39,3 +40,44 @@ def create_perturbation(
39
40
  else:
40
41
  builder = PerturbationBuilder()
41
42
  return builder.get_material(configuration)
43
+
44
+
45
+ def create_maxwell_displacement(
46
+ material: Union[Material, MaterialWithBuildMetadata],
47
+ disorder_parameter: float,
48
+ random_seed: Optional[int] = None,
49
+ is_mass_used: bool = True,
50
+ ) -> Material:
51
+ """
52
+ Apply Maxwell-Boltzmann random displacements to a material.
53
+
54
+ Generates random 3D displacement vectors where each component follows a normal
55
+ distribution with variance proportional to disorder_parameter/m (if is_mass_used=True)
56
+ or disorder_parameter (if is_mass_used=False), where m is atomic mass.
57
+
58
+ Args:
59
+ material: The material to be perturbed.
60
+ disorder_parameter: Disorder parameter controlling displacement magnitude,
61
+ can be viewed as effective temperature in eV.
62
+ random_seed: Optional random seed for reproducibility for the same material and parameters.
63
+ is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent).
64
+ If False, displacement variance is disorder_parameter (mass-independent).
65
+
66
+ Returns:
67
+ Material with applied Maxwell-Boltzmann displacements.
68
+ """
69
+ displacement_holder = MaxwellBoltzmannDisplacementHolder(
70
+ disorder_parameter=disorder_parameter,
71
+ random_seed=random_seed,
72
+ is_mass_used=is_mass_used,
73
+ )
74
+
75
+ configuration = PerturbationConfiguration(
76
+ material=material,
77
+ perturbation_function_holder=displacement_holder,
78
+ use_cartesian_coordinates=True,
79
+ )
80
+
81
+ builder = PerturbationBuilder()
82
+
83
+ return builder.get_material(configuration)
@@ -2,10 +2,19 @@ import inspect
2
2
  from functools import wraps
3
3
  from typing import Any, Callable, Dict, Union
4
4
 
5
- from mat3ra.made.material import Material
6
- from mat3ra.made.utils import map_array_to_array_with_id_value, map_array_with_id_value_to_array
7
5
  from mat3ra.utils.mixins import RoundNumericValuesMixin
8
6
 
7
+ from mat3ra.made.material import Material
8
+ from mat3ra.made.utils import (
9
+ map_array_to_array_with_id_value,
10
+ map_array_with_id_value_to_array,
11
+ )
12
+ from .utils import (
13
+ extract_labels_from_pymatgen_structure,
14
+ extract_metadata_from_pymatgen_structure,
15
+ extract_tags_from_ase_atoms,
16
+ calculate_padded_cell_simple_cubic,
17
+ )
9
18
  from ..third_party import (
10
19
  ASEAtoms,
11
20
  PymatgenAseAtomsAdaptor,
@@ -14,11 +23,6 @@ from ..third_party import (
14
23
  PymatgenPoscar,
15
24
  PymatgenStructure,
16
25
  )
17
- from .utils import (
18
- extract_labels_from_pymatgen_structure,
19
- extract_metadata_from_pymatgen_structure,
20
- extract_tags_from_ase_atoms,
21
- )
22
26
 
23
27
 
24
28
  def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> PymatgenStructure:
@@ -198,15 +202,27 @@ def from_ase(ase_atoms: ASEAtoms) -> Dict[str, Any]:
198
202
  Returns:
199
203
  dict: A dictionary containing the material information in ESSE format.
200
204
  """
205
+ is_molecule = not any(ase_atoms.pbc)
206
+
207
+ if is_molecule:
208
+ lattice_vectors = calculate_padded_cell_simple_cubic(ase_atoms.get_positions())
209
+ ase_atoms.set_cell(lattice_vectors)
210
+ ase_atoms.center()
211
+
201
212
  # TODO: check that atomic labels/tags are properly handled
202
213
  structure = PymatgenAseAtomsAdaptor.get_structure(ase_atoms)
214
+
203
215
  material = from_pymatgen(structure)
216
+ material["isNonPeriodic"] = is_molecule
217
+ if is_molecule:
218
+ material["lattice"]["type"] = "CUB"
219
+
204
220
  ase_tags = extract_tags_from_ase_atoms(ase_atoms)
205
221
  material["basis"]["labels"] = ase_tags
206
222
  ase_metadata = ase_atoms.info.get("metadata", {})
207
223
  if ase_metadata:
208
224
  material["metadata"].update(ase_metadata)
209
- material["name"] = ase_atoms.info.get("name", "")
225
+ material["name"] = ase_atoms.info.get("name", ase_atoms.get_chemical_formula())
210
226
  return material
211
227
 
212
228
 
@@ -1,11 +1,13 @@
1
1
  import json
2
2
  from typing import Any, Dict, List, Union
3
3
 
4
- from mat3ra.made.utils import map_array_to_array_with_id_value
4
+ import numpy as np
5
5
  from mat3ra.utils.object import NumpyNDArrayRoundEncoder
6
+ from scipy.spatial.distance import pdist
6
7
 
7
- from ..third_party import ASEAtoms, PymatgenInterface, PymatgenStructure
8
+ from mat3ra.made.utils import map_array_to_array_with_id_value, get_center_of_coordinates
8
9
  from .interface_parts_enum import INTERFACE_LABELS_MAP
10
+ from ..third_party import ASEAtoms, PymatgenInterface, PymatgenStructure
9
11
 
10
12
 
11
13
  def extract_labels_from_pymatgen_structure(structure: PymatgenStructure) -> List[int]:
@@ -37,3 +39,27 @@ def extract_tags_from_ase_atoms(atoms: ASEAtoms) -> List[Union[str, int]]:
37
39
  int_tags = [int(tag) for tag in atoms.arrays["tags"] if tag is not None]
38
40
  result = map_array_to_array_with_id_value(int_tags, remove_none=True)
39
41
  return result
42
+
43
+
44
+ def calculate_padded_cell_simple_cubic(
45
+ coordinates: List[List[float]], padding_factor: float = 2.0
46
+ ) -> List[List[float]]:
47
+ """
48
+ Calculate values for a padded cell for a molecule based on its coordinates.
49
+ Args:
50
+ coordinates (Array[Array[float]]): A list of atomic coordinates.
51
+ padding_factor (float): The factor by which to multiply the maximum distance for padding.
52
+ Returns:
53
+ Array[float]: A list containing the final cell latice vectors with padding applied.
54
+ """
55
+ positions = np.array(coordinates)
56
+ center = get_center_of_coordinates(positions)
57
+ shifted_positions = positions - center
58
+ max_distance = np.max(pdist(shifted_positions)) if len(positions) >= 2 else 10.0
59
+ padding_value = padding_factor * max_distance
60
+
61
+ return [
62
+ [padding_value, 0.0, 0.0],
63
+ [0.0, padding_value, 0.0],
64
+ [0.0, 0.0, padding_value],
65
+ ]
@@ -84,12 +84,11 @@ def perturb(
84
84
  perturbed_coordinates: List[List[float]] = []
85
85
 
86
86
  for coordinate in original_coordinates:
87
- # If func_holder returns a scalar, assume z-axis; otherwise vector
88
- displacement = perturbation_function.apply_function(coordinate)
87
+ displacement = perturbation_function.apply_function(coordinate, material=new_material)
88
+
89
89
  if isinstance(displacement, (list, tuple, np.ndarray)):
90
90
  delta = np.array(displacement)
91
91
  else:
92
- # scalar: apply to z-axis
93
92
  delta = np.array([0.0, 0.0, displacement])
94
93
 
95
94
  new_coordinate = np.array(coordinate) + delta
@@ -0,0 +1,14 @@
1
+ from ase.build import bulk, molecule
2
+
3
+
4
+ BULK_SI_LATTICE_A = 3.8395
5
+ BULK_SI_LATTICE_ALPHA = 60
6
+ BULK_SI_LABELS = [{"id": 0, "value": 0}, {"id": 1, "value": 1}]
7
+
8
+ atoms = bulk("Si")
9
+ atoms.set_tags([0, 1])
10
+ ASE_BULK_Si = atoms
11
+
12
+ atoms = molecule("H2O")
13
+ atoms.set_pbc(False)
14
+ ASE_MOLECULE_H2O = atoms
@@ -0,0 +1,123 @@
1
+ import numpy as np
2
+ import pytest
3
+ from mat3ra.made.material import Material
4
+ from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import (
5
+ MaxwellBoltzmannDisplacementHolder,
6
+ )
7
+ from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import create_maxwell_displacement
8
+ from mat3ra.made.tools.helpers import create_supercell
9
+ from mat3ra.periodic_table.helpers import get_atomic_mass_from_element
10
+
11
+ from .fixtures.bulk import BULK_Si_PRIMITIVE
12
+ from .fixtures.slab import SI_CONVENTIONAL_SLAB_001
13
+
14
+ DISORDER_PARAMETER = 1.0 # Temperature-like
15
+ RANDOM_SEED = 42
16
+ NUM_SAMPLES_FOR_MSD = 1000
17
+
18
+
19
+ @pytest.mark.parametrize("random_seed", [None, 42, 123])
20
+ def test_maxwell_displacement_deterministic(random_seed):
21
+ material = Material.create(BULK_Si_PRIMITIVE)
22
+ displacement_func1 = MaxwellBoltzmannDisplacementHolder(
23
+ disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed
24
+ )
25
+ displacement_func2 = MaxwellBoltzmannDisplacementHolder(
26
+ disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed
27
+ )
28
+
29
+ coord = [0.0, 0.0, 0.0]
30
+
31
+ if random_seed is not None:
32
+ disp1 = displacement_func1.apply_function(coord, material=material)
33
+ disp2 = displacement_func2.apply_function(coord, material=material)
34
+ assert np.allclose(disp1, disp2)
35
+
36
+ # Different seed should give different results
37
+ displacement_func3 = MaxwellBoltzmannDisplacementHolder(
38
+ disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed + 1
39
+ )
40
+ disp3 = displacement_func3.apply_function(coord, material=material)
41
+ assert not np.allclose(disp1, disp3) or np.allclose(disp1, [0, 0, 0], atol=1e-10)
42
+ else:
43
+ # No seed: different instances should give different results (non-deterministic)
44
+ disp1 = displacement_func1.apply_function(coord, material=material)
45
+ disp2 = displacement_func2.apply_function(coord, material=material)
46
+ assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10)
47
+
48
+
49
+ def test_maxwell_displacement_perturb_integration():
50
+ material = Material.create(BULK_Si_PRIMITIVE)
51
+ original_coords = [coord[:] for coord in material.basis.coordinates.values]
52
+
53
+ perturbed_material = create_maxwell_displacement(
54
+ material, disorder_parameter=DISORDER_PARAMETER, random_seed=RANDOM_SEED
55
+ )
56
+
57
+ assert len(perturbed_material.basis.coordinates.values) == len(original_coords)
58
+ for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)):
59
+ delta = np.array(pert) - np.array(orig)
60
+ assert np.linalg.norm(delta) > 0 or np.allclose(delta, 0, atol=1e-10)
61
+
62
+
63
+ def test_maxwell_displacement_msd_expectation():
64
+ material = Material.create(BULK_Si_PRIMITIVE)
65
+ si_mass = get_atomic_mass_from_element("Si")
66
+ disorder_parameter = DISORDER_PARAMETER
67
+ expected_variance = disorder_parameter / si_mass
68
+ expected_msd = 3 * expected_variance
69
+
70
+ displacements = []
71
+ coord = [0.0, 0.0, 0.0]
72
+ for _ in range(NUM_SAMPLES_FOR_MSD):
73
+ displacement_func = MaxwellBoltzmannDisplacementHolder(disorder_parameter=disorder_parameter, random_seed=None)
74
+ disp = displacement_func.apply_function(coord, material=material)
75
+ displacements.append(disp)
76
+
77
+ displacements_array = np.array(displacements)
78
+ msd = np.mean(np.sum(displacements_array**2, axis=1))
79
+
80
+ assert abs(msd - expected_msd) / expected_msd < 0.3
81
+
82
+
83
+ @pytest.mark.parametrize(
84
+ "slab_config, temperature_k, random_seed",
85
+ [
86
+ (SI_CONVENTIONAL_SLAB_001, 1300.0, 42),
87
+ (SI_CONVENTIONAL_SLAB_001, 1300.0, 42),
88
+ ],
89
+ )
90
+ def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed):
91
+ material = Material.create(slab_config)
92
+ material = create_supercell(material, scaling_factor=[4, 4, 1])
93
+ original_coords = [coord[:] for coord in material.basis.coordinates.values]
94
+ original_lattice = material.lattice.vector_arrays.copy()
95
+
96
+ perturbed_material = create_maxwell_displacement(
97
+ material, disorder_parameter=temperature_k, random_seed=random_seed
98
+ )
99
+
100
+ assert len(perturbed_material.basis.coordinates.values) == len(original_coords)
101
+ assert len(perturbed_material.basis.elements.values) == len(material.basis.elements.values)
102
+
103
+ coordinate_changes = []
104
+ for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)):
105
+ delta = np.array(pert) - np.array(orig)
106
+ displacement_magnitude = np.linalg.norm(delta)
107
+ coordinate_changes.append(displacement_magnitude)
108
+
109
+ max_displacement = max(coordinate_changes)
110
+ mean_displacement = np.mean(coordinate_changes)
111
+
112
+ assert max_displacement > 0
113
+ assert mean_displacement > 0
114
+
115
+ si_mass = get_atomic_mass_from_element("Si")
116
+ expected_std = np.sqrt(temperature_k / si_mass)
117
+
118
+ assert mean_displacement < 5 * expected_std
119
+
120
+ assert np.allclose(perturbed_material.lattice.vector_arrays, original_lattice, atol=1e-10)
121
+
122
+ for i, element in enumerate(material.basis.elements.values):
123
+ assert perturbed_material.basis.elements.values[i] == element
@@ -1,22 +1,13 @@
1
1
  import pytest
2
- from mat3ra.code.entity import InMemoryEntity, InMemoryEntityPydantic
2
+ from mat3ra.code.entity import InMemoryEntityPydantic
3
+
3
4
  from mat3ra.made.tools.build_components.metadata import BuildMetadata, MaterialBuildMetadata
4
- from pydantic import BaseModel
5
5
 
6
6
 
7
7
  class ConfigWithToDict(InMemoryEntityPydantic):
8
8
  value: str = "test_dict"
9
9
 
10
10
 
11
- # TODO: Remove this class when all configurations moved to Pydantic
12
- class ConfigWithToJsonReturnsDict(BaseModel, InMemoryEntity):
13
- value: str = "test_json_dict"
14
-
15
- @property
16
- def _json(self):
17
- return {"value": self.value}
18
-
19
-
20
11
  class ConfigWithToJsonReturnsStr(InMemoryEntityPydantic):
21
12
  value: str = "test_json_str"
22
13
 
@@ -42,7 +33,6 @@ def test_metadata_empty_initialization():
42
33
  "config_object, expected_dict",
43
34
  [
44
35
  (ConfigWithToDict(), {"value": "test_dict"}),
45
- (ConfigWithToJsonReturnsDict(), {"value": "test_json_dict"}),
46
36
  (ConfigWithToJsonReturnsStr(), {"value": "test_json_str"}),
47
37
  ],
48
38
  )
@@ -1,14 +1,20 @@
1
1
  import numpy as np
2
2
  import pytest
3
3
  from ase import Atoms
4
- from ase.build import bulk
5
4
  from mat3ra.code.array_with_ids import ArrayWithIds
6
- from mat3ra.made.material import Material
7
- from mat3ra.made.tools.convert import from_ase, from_poscar, from_pymatgen, to_ase, to_poscar, to_pymatgen
8
5
  from mat3ra.utils import assertion as assertion_utils
9
6
  from pymatgen.core.structure import Element, Lattice, Structure
10
7
 
8
+ from mat3ra.made.material import Material
9
+ from mat3ra.made.tools.convert import from_ase, from_poscar, from_pymatgen, to_ase, to_poscar, to_pymatgen
11
10
  from .fixtures.monolayer import GRAPHENE
11
+ from .fixtures.thrid_party.ase_atoms import (
12
+ ASE_BULK_Si,
13
+ ASE_MOLECULE_H2O,
14
+ BULK_SI_LATTICE_A,
15
+ BULK_SI_LATTICE_ALPHA,
16
+ BULK_SI_LABELS,
17
+ )
12
18
 
13
19
  PYMATGEN_LATTICE = Lattice.from_parameters(a=3.84, b=3.84, c=3.84, alpha=120, beta=90, gamma=60)
14
20
  PYMATGEN_STRUCTURE = Structure(PYMATGEN_LATTICE, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]])
@@ -98,10 +104,42 @@ def test_to_ase():
98
104
  assert ase_atoms.get_tags().tolist() == [0, 1]
99
105
 
100
106
 
101
- def test_from_ase():
102
- ase_atoms = bulk("Si")
103
- ase_atoms.set_tags([0, 1])
107
+ @pytest.mark.parametrize(
108
+ "ase_atoms, expected_lattice_a, expected_lattice_alpha, expected_labels,"
109
+ + " expected_is_non_periodic, expected_lattice_type",
110
+ [
111
+ (
112
+ ASE_BULK_Si,
113
+ BULK_SI_LATTICE_A,
114
+ BULK_SI_LATTICE_ALPHA,
115
+ BULK_SI_LABELS,
116
+ False,
117
+ None,
118
+ ),
119
+ (
120
+ ASE_MOLECULE_H2O,
121
+ None,
122
+ None,
123
+ [],
124
+ True,
125
+ "CUB",
126
+ ),
127
+ ],
128
+ )
129
+ def test_from_ase(
130
+ ase_atoms,
131
+ expected_lattice_a,
132
+ expected_lattice_alpha,
133
+ expected_labels,
134
+ expected_is_non_periodic,
135
+ expected_lattice_type,
136
+ ):
104
137
  material_data = from_ase(ase_atoms)
105
- assert material_data["lattice"]["a"] == 3.839589822
106
- assert material_data["lattice"]["alpha"] == 60
107
- assert material_data["basis"]["labels"] == [{"id": 0, "value": 0}, {"id": 1, "value": 1}]
138
+ if expected_lattice_a is not None:
139
+ assertion_utils.assert_almost_equal_numbers(material_data["lattice"]["a"], expected_lattice_a, atol=1e-4)
140
+ if expected_lattice_alpha is not None:
141
+ assert material_data["lattice"]["alpha"] == expected_lattice_alpha
142
+ assert material_data["basis"]["labels"] == expected_labels
143
+ assert material_data["isNonPeriodic"] == expected_is_non_periodic
144
+ if expected_lattice_type is not None:
145
+ assert material_data["lattice"]["type"] == expected_lattice_type