@mat3ra/made 2024.4.8-0 → 2024.4.16-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/README.md CHANGED
@@ -5,15 +5,40 @@
5
5
 
6
6
  Made is a library for **MA**terials **DE**sign in JavaScript/TypeScript and Python. It allows for the creation and manipulation of material structures from atoms up on the web. The library is aimed to be used for the development of web applications, both on the client (web browser) and server (eg. Node.js) side.
7
7
 
8
- The library was originally designed as part of and presently powers materials design capabilities of the [Exabyte.io](https://exabyte.io) platform. For example, [this page](https://platform.exabyte.io/demo/materials/n3HSzCmyoctgJFGGE) representing a crystal of Silicon online uses Made.js.
8
+ ## 1. Overview
9
9
 
10
- Exabyte.io believe in a collaborative future of materials design on the web.
10
+ The package provides a software environment for interacting with Materials-related data structures from ESSE Data Convention [[1]](#links) for use on the web.
11
11
 
12
- ## Functionality
12
+ ## 2. Installation
13
13
 
14
- As below:
14
+ ### 2.1. JavaScript/TypeScript
15
+
16
+ From NPM for use within a software project:
17
+
18
+ ```bash
19
+ npm install @mat3ra/made
20
+
21
+ ```
22
+
23
+ ### 2.2. Python
24
+
25
+ From PyPI for use within a software project:
26
+
27
+ ```bash
28
+ pip install mat3ra-made
29
+ ```
30
+
31
+ When willing to use the optional `tools` module, install the package with the following command:
32
+
33
+ ```bash
34
+ pip install "mat3ra-made[tools]"
35
+ ```
36
+
37
+
38
+ ## 3. Functionality
39
+
40
+ As below
15
41
 
16
- - the package provides a software environment for interacting with Materials-related data structures from ESSE Data Convention [[1]](#links) and is written in ECMAScript 2015 for use on the web
17
42
  - High-level classes for the representation of the [Material](src/material.js) and the corresponding structural information, ie:
18
43
  - [Basis](src/basis/basis.js),
19
44
  - [Lattice](src/lattice/lattice.js),
@@ -32,38 +57,18 @@ As below:
32
57
  - [combinatorial sets](src/parsers/xyz_combinatorial_basis.js)
33
58
  - [interpolated sets for chemical reactions](src/tools/basis.js)
34
59
 
35
- The package is written in a modular way easy to extend. Contributions can be in the form of additional tools or modules you develop, or feature requests and [bug/issue reports](https://help.github.com/articles/creating-an-issue/).
36
-
37
- ## Installation
38
-
39
- From NPM for use within a software project:
40
-
41
- ```bash
42
- npm install @mat3ra/made
43
-
44
- ```
45
60
 
46
- From source to contribute to development:
47
-
48
- ```bash
49
- git clone git@github.com:Exabyte-io/made
50
- ```
51
-
52
- ## Contribution
61
+ ## 4. Contribution
53
62
 
54
63
  This repository is an [open-source](LICENSE.md) work-in-progress and we welcome contributions.
55
64
 
56
- ### Why contribute?
57
-
58
65
  We regularly deploy the latest code containing all accepted contributions online as part of the [Mat3ra.com](https://mat3ra.com) platform, so contributors will see their code in action there.
59
66
 
60
- ### Adding new functionality
61
-
62
67
  We suggest forking this repository and introducing the adjustments there to be considered for merging into this repository as explained in more details [here](https://gist.github.com/Chaser324/ce0505fbed06b947d962), for example.
63
68
 
64
- ### Source code conventions
69
+ ### 4.1. Source code conventions
65
70
 
66
- Made.js is written in EcmaScript 6th edition [[2]](#links) with the application of object-oriented design patterns encapsulating key concepts following the conventions below.
71
+ Object-oriented design patterns encapsulate key concepts following the conventions below.
67
72
 
68
73
  1. Classes follow the Exabyte Data Convention and data structures defined in ESSE [[1]](#links)
69
74
 
@@ -71,12 +76,12 @@ Made.js is written in EcmaScript 6th edition [[2]](#links) with the application
71
76
 
72
77
  3. `tools` directory contains helper functions that act on one or more classes and include an external parameter. Functions that use class data without any external parameters should be implemented inside the class. For example, `basis.clone()` is implemented in `Basis`, but basis repetition is implemented as a tool in the correspondingly named function ([tools/basis.js#repeat](src/tools/basis.js)) because the repetion requires a parameter external to basis - number of repetitions in 3 spatial dimensions.
73
78
 
74
- 4. `parsers` directory contains the parsers to- and from- ESSE format mentioned in 1. All functionality related to external data conversion is contained in this directory.
79
+ 4. [Deprecated, use mat3ra-parsers or @mat3ra/parsers] `parsers` directory contains the parsers to- and from- ESSE format mentioned in 1. All functionality related to external data conversion is contained in this directory.
75
80
 
76
81
 
77
- ### TODO list
82
+ ### 4.2. TODO list
78
83
 
79
- Desirable features for implementation:
84
+ [Outdated] Desirable features for implementation:
80
85
 
81
86
  - identify primitive / conventional structures
82
87
  - support for molecular geometries
@@ -90,7 +95,11 @@ Desirable features for implementation:
90
95
  - a combination of the above
91
96
  - arbitrary atomic arrangement
92
97
 
93
- ## Tests
98
+ ## 5. Development
99
+
100
+ ### 5.1. JavaScript/TypeScript
101
+
102
+ #### 5.1.1. Tests
94
103
 
95
104
  Made tests are written based on Mocha [6](#links) testing framework and can be executed as follows.
96
105
 
@@ -105,7 +114,7 @@ npm install
105
114
  npm test
106
115
  ```
107
116
 
108
- ### Tests Important Notes
117
+ #### 5.1.2. Important Notes
109
118
 
110
119
  1. Keep the tests directory structure similar to the main codebase directory structure. Every JS module in the main codebase should have a corresponding module in tests directory which implements the tests for provided functionality.
111
120
 
@@ -115,7 +124,7 @@ npm test
115
124
 
116
125
  4. [Tests setup module](./tests/setup.js) can be used to implement the hooks that are used to prepare the tests environment.
117
126
 
118
- ## Using Linter
127
+ #### 5.1.3. Using Linter
119
128
 
120
129
  Linter setup will prevent committing files that don't adhere to the code standard. It will
121
130
  attempt to fix what it can automatically prior to the commit in order to reduce diff noise. This can lead to "unexpected" behavior where a
@@ -149,13 +158,44 @@ npm run lint:fix
149
158
  In which case, you may need to then add the linter edits to your staging, which in the example above, puts the
150
159
  file back to identical with the base branch, resulting in no staged changes whatsoever.
151
160
 
152
- ## Configuring WebStorm for use with Linter
161
+ #### 5.1.4. Configuring WebStorm for use with Linter
153
162
 
154
163
  In order for the WebStorm IDE to take full advantage of the linting configuration, it can be configured in the project:
155
164
 
156
165
  - `Preferences -> Languages & Frameworks -> JavaScript -> Code Quality Tools -> ESLint`
157
166
  - Check `Automatic ESLint configuration` which should infer all the configurations from the project directory
158
167
 
168
+ ### 5.2. Python
169
+
170
+ #### 5.2.1. Tests
171
+
172
+ Python 3.8+ is required to run the tests. We recommend using PyEnv to manage Python versions. Tests are written based on PyTest and can be executed as follows.
173
+
174
+ ```bash
175
+ virtualenv .venv
176
+ source .venv/bin/activate
177
+ pip install ".[tests]"
178
+ pytest tests/py
179
+ ```
180
+
181
+ #### 5.2.2. Important Notes
182
+
183
+ Conventions:
184
+
185
+ - The "tools" module has external dependencies on "pymatgen" and "ase" packages and so is meant as optional. When implementing new functionality, the use of ASE is recommended over pymatgen for compatibility purposes.
186
+
187
+ ### 5.3. Known Issues
188
+
189
+ #### 5.3.1. JavaScript/TypeScript
190
+
191
+ To be added.
192
+
193
+ #### 5.3.2. Python
194
+
195
+ As below:
196
+
197
+ - Python 3.8 tests are failing on Apple Silicon due to https://github.com/materialsproject/pymatgen/issues/3521
198
+
159
199
  ## Links
160
200
 
161
201
  1. [Exabyte Source of Schemas and Examples (ESSE), Github Repository](https://github.com/exabyte-io/exabyte-esse)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2024.4.8-0",
3
+ "version": "2024.4.16-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
@@ -17,34 +17,36 @@ classifiers = [
17
17
  dependencies = [
18
18
  # add requirements here
19
19
  "numpy",
20
- "pymatgen",
21
- "ase",
22
20
  "mat3ra-esse",
23
21
  "mat3ra-code",
24
22
  ]
25
23
 
26
24
  [project.optional-dependencies]
27
- tests = [
28
- "coverage[toml]>=5.3",
25
+ # tracking separately the deps required to use the tools module
26
+ tools = [
27
+ "pymatgen",
28
+ "ase",
29
+ ]
30
+ dev = [
29
31
  "pre-commit",
30
32
  "black",
31
33
  "ruff",
32
34
  "isort",
33
35
  "mypy",
34
36
  "pip-tools",
37
+ ]
38
+ tests = [
39
+ "coverage[toml]>=5.3",
35
40
  "pytest",
36
41
  "pytest-cov",
37
- "pydantic",
42
+ # B/c of https://github.com/binary-husky/gpt_academic/issues/1237
38
43
  "gradio",
39
- ]
40
- # required to use the tools module
41
- tools = [
42
- "pymatgen",
43
- "ase",
44
+ "pydantic",
45
+ "mat3ra-made[tools]",
44
46
  ]
45
47
  all = [
46
48
  "mat3ra-made[tests]",
47
- "mat3ra-made[tools]",
49
+ "mat3ra-made[dev]",
48
50
  ]
49
51
 
50
52
  # Entrypoint scripts can be defined here, see examples below.
@@ -0,0 +1,41 @@
1
+ import numpy as np
2
+ from ase import Atoms
3
+
4
+ from .convert import convert_material_args_kwargs_to_atoms
5
+
6
+
7
+ @convert_material_args_kwargs_to_atoms
8
+ def calculate_average_interlayer_distance(
9
+ interface_atoms: Atoms, tag_substrate: str, tag_film: str, threshold: float = 0.5
10
+ ) -> float:
11
+ """
12
+ Calculate the average distance between the top layer of substrate atoms and the bottom layer of film atoms.
13
+
14
+ Args:
15
+ interface_atoms (ase.Atoms): The ASE Atoms object containing both sets of atoms.
16
+ tag_substrate (int): The tag representing the substrate atoms.
17
+ tag_film (int): The tag representing the film atoms.
18
+ threshold (float): The threshold for identifying the top and bottom layers of atoms.
19
+
20
+ Returns:
21
+ float: The average distance between the top layer of substrate and the bottom layer of film.
22
+ """
23
+ # Extract z-coordinates of substrate and film atoms
24
+ z_substrate = interface_atoms.positions[interface_atoms.get_tags() == tag_substrate][:, 2]
25
+ z_film = interface_atoms.positions[interface_atoms.get_tags() == tag_film][:, 2]
26
+
27
+ # Identify the top layer of substrate atoms and bottom layer of film atoms by z-coordinate
28
+ top_substrate_layer_z = np.max(z_substrate)
29
+ bottom_film_layer_z = np.min(z_film)
30
+
31
+ # Get the average z-coordinate of the top substrate atoms (within a threshold from the top)
32
+ top_substrate_atoms = z_substrate[z_substrate >= top_substrate_layer_z - threshold]
33
+ avg_z_top_substrate = np.mean(top_substrate_atoms)
34
+
35
+ # Get the average z-coordinate of the bottom film atoms (within a threshold from the bottom)
36
+ bottom_film_atoms = z_film[z_film <= bottom_film_layer_z + threshold]
37
+ avg_z_bottom_film = np.mean(bottom_film_atoms)
38
+
39
+ # Calculate the average distance between the top layer of substrate and the bottom layer of film
40
+ average_interlayer_distance = avg_z_bottom_film - avg_z_top_substrate
41
+ return abs(average_interlayer_distance)
@@ -0,0 +1,20 @@
1
+ from ase import Atoms
2
+ from ase.calculators.calculator import Calculator
3
+
4
+ from .convert import convert_material_args_kwargs_to_atoms
5
+
6
+
7
+ @convert_material_args_kwargs_to_atoms
8
+ def calculate_total_energy(atoms: Atoms, calculator: Calculator):
9
+ """
10
+ Set calculator for ASE Atoms and calculate the total energy.
11
+
12
+ Args:
13
+ atoms (ase.Atoms): The Atoms object to calculate the energy of.
14
+ calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
15
+
16
+ Returns:
17
+ float: The total energy of the atoms.
18
+ """
19
+ atoms.set_calculator(calculator)
20
+ return atoms.get_total_energy()
@@ -0,0 +1,186 @@
1
+ import inspect
2
+ from functools import wraps
3
+ from typing import Any, Callable, Dict, Union
4
+
5
+ from ase import Atoms
6
+ from mat3ra.made.material import Material
7
+ from mat3ra.utils.mixins import RoundNumericValuesMixin
8
+ from pymatgen.core.structure import Lattice, Structure
9
+ from pymatgen.io.ase import AseAtomsAdaptor
10
+ from pymatgen.io.vasp.inputs import Poscar
11
+
12
+
13
+ def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> Structure:
14
+ """
15
+ Converts material object in ESSE format to a pymatgen Structure object.
16
+
17
+ Args:
18
+ material_or_material_data (dict|class): A dictionary containing the material information in ESSE format.
19
+
20
+ Returns:
21
+ Structure: A pymatgen Structure object.
22
+ """
23
+ material_data = material_or_material_data
24
+
25
+ if isinstance(material_or_material_data, Material):
26
+ material_data = material_or_material_data.to_json()
27
+
28
+ lattice_params = material_data["lattice"]
29
+ a = lattice_params["a"]
30
+ b = lattice_params["b"]
31
+ c = lattice_params["c"]
32
+ alpha = lattice_params["alpha"]
33
+ beta = lattice_params["beta"]
34
+ gamma = lattice_params["gamma"]
35
+ lattice = Lattice.from_parameters(a, b, c, alpha, beta, gamma)
36
+
37
+ basis = material_data["basis"]
38
+ elements = [element["value"] for element in basis["elements"]]
39
+ coordinates = [coord["value"] for coord in basis["coordinates"]]
40
+ labels = [label["value"] for label in basis.get("labels", [])]
41
+
42
+ # Assuming that the basis units are fractional since it's a crystal basis
43
+ coords_are_cartesian = "units" in basis and basis["units"].lower() == "angstrom"
44
+
45
+ if "labels" in inspect.signature(Structure.__init__).parameters:
46
+ structure = Structure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian, labels=labels)
47
+ else:
48
+ # Passing labels does not work for pymatgen `2023.6.23` supporting py3.8
49
+ print(f"labels: {labels}. Not passing labels to pymatgen.")
50
+ structure = Structure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian)
51
+
52
+ return structure
53
+
54
+
55
+ def from_pymatgen(structure: Structure):
56
+ """
57
+ Converts a pymatgen Structure object to a material object in ESSE format.
58
+
59
+ Args:
60
+ structure (Structure): A pymatgen Structure object.
61
+
62
+ Returns:
63
+ dict: A dictionary containing the material information in ESSE format.
64
+ """
65
+ __round__ = RoundNumericValuesMixin.round_array_or_number
66
+ basis = {
67
+ "elements": [{"id": i, "value": str(site.specie)} for i, site in enumerate(structure.sites)],
68
+ "coordinates": [
69
+ {"id": i, "value": __round__(list(site.frac_coords))} for i, site in enumerate(structure.sites)
70
+ ],
71
+ "units": "crystal",
72
+ "cell": __round__(structure.lattice.matrix.tolist()),
73
+ "constraints": [],
74
+ }
75
+
76
+ lattice = {
77
+ "a": __round__(structure.lattice.a),
78
+ "b": __round__(structure.lattice.b),
79
+ "c": __round__(structure.lattice.c),
80
+ "alpha": __round__(structure.lattice.alpha),
81
+ "beta": __round__(structure.lattice.beta),
82
+ "gamma": __round__(structure.lattice.gamma),
83
+ "units": {"length": "angstrom", "angle": "degree"},
84
+ "type": "TRI",
85
+ "vectors": {
86
+ "a": __round__(structure.lattice.matrix[0].tolist()),
87
+ "b": __round__(structure.lattice.matrix[1].tolist()),
88
+ "c": __round__(structure.lattice.matrix[2].tolist()),
89
+ "alat": 1,
90
+ "units": "angstrom",
91
+ },
92
+ }
93
+
94
+ material_data = {
95
+ "name": structure.formula,
96
+ "basis": basis,
97
+ "lattice": lattice,
98
+ "isNonPeriodic": not structure.is_ordered,
99
+ "_id": "",
100
+ "metadata": {"boundaryConditions": {"type": "pbc", "offset": 0}},
101
+ "isUpdated": True,
102
+ }
103
+
104
+ return material_data
105
+
106
+
107
+ def to_poscar(material_or_material_data: Union[Material, Dict[str, Any]]) -> str:
108
+ """
109
+ Converts material object in ESSE format to a POSCAR string.
110
+
111
+ Args:
112
+ material_or_material_data (dict|class): A dictionary containing the material information in ESSE format.
113
+
114
+ Returns:
115
+ str: A POSCAR string.
116
+ """
117
+ structure = to_pymatgen(material_or_material_data)
118
+ poscar = Poscar(structure)
119
+ # For pymatgen `2023.6.23` supporting py3.8 the method name is "get_string"
120
+ # TODO: cleanup the if statement when dropping support for py3.8
121
+ if hasattr(poscar, "get_string"):
122
+ return poscar.get_string()
123
+ return poscar.get_str()
124
+
125
+
126
+ def from_poscar(poscar: str) -> Dict[str, Any]:
127
+ """
128
+ Converts a POSCAR string to a material object in ESSE format.
129
+
130
+ Args:
131
+ poscar (str): A POSCAR string.
132
+
133
+ Returns:
134
+ dict: A dictionary containing the material information in ESSE format.
135
+ """
136
+ structure = Structure.from_str(poscar, "poscar")
137
+ return from_pymatgen(structure)
138
+
139
+
140
+ def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> Atoms:
141
+ """
142
+ Converts material object in ESSE format to an ASE Atoms object.
143
+
144
+ Args:
145
+ material_or_material_data (dict|class): A dictionary containing the material information in ESSE format.
146
+
147
+ Returns:
148
+ Any: An ASE Atoms object.
149
+ """
150
+ # TODO: check that atomic labels are properly handled
151
+ structure = to_pymatgen(material_or_material_data)
152
+ return AseAtomsAdaptor.get_atoms(structure)
153
+
154
+
155
+ def from_ase(ase_atoms: Atoms) -> Dict[str, Any]:
156
+ """
157
+ Converts an ASE Atoms object to a material object in ESSE format.
158
+
159
+ Args:
160
+ ase_atoms (Atoms): An ASE Atoms object.
161
+
162
+ Returns:
163
+ dict: A dictionary containing the material information in ESSE format.
164
+ """
165
+ # TODO: check that atomic labels/tags are properly handled
166
+ structure = AseAtomsAdaptor.get_structure(ase_atoms)
167
+ return from_pymatgen(structure)
168
+
169
+
170
+ def convert_material_args_kwargs_to_atoms(func: Callable) -> Callable:
171
+ """
172
+ Decorator that converts ESSE Material objects to ASE Atoms objects.
173
+ """
174
+
175
+ @wraps(func)
176
+ def wrapper(*args, **kwargs):
177
+ # Convert args if they are of type ESSE Material
178
+ new_args = [to_ase(arg) if isinstance(arg, Material) else arg for arg in args]
179
+
180
+ # Convert kwargs if they are of type ESSE Material
181
+ new_kwargs = {k: to_ase(v) if isinstance(v, Material) else v for k, v in kwargs.items()}
182
+
183
+ # Call the original function with the converted arguments
184
+ return func(*new_args, **new_kwargs)
185
+
186
+ return wrapper
@@ -0,0 +1,20 @@
1
+ from typing import Union
2
+
3
+ from ase import Atoms
4
+
5
+ from .convert import convert_material_args_kwargs_to_atoms
6
+
7
+
8
+ @convert_material_args_kwargs_to_atoms
9
+ def filter_by_label(atoms: Atoms, label: Union[int, str]):
10
+ """
11
+ Filter out only atoms corresponding to the label/tag.
12
+
13
+ Args:
14
+ atoms (ase.Atoms): The Atoms object to filter.
15
+ label (int|str): The tag/label to filter by.
16
+
17
+ Returns:
18
+ ase.Atoms: The filtered Atoms object.
19
+ """
20
+ return atoms[atoms.get_tags() == label]
@@ -0,0 +1,12 @@
1
+ import numpy as np
2
+ from ase.build import bulk
3
+ from mat3ra.made.tools.analyze import calculate_average_interlayer_distance
4
+
5
+
6
+ def test_calculate_average_interlayer_distance():
7
+ substrate = bulk("Si", cubic=True)
8
+ film = bulk("Cu", cubic=True)
9
+ interface_atoms = substrate + film
10
+ interface_atoms.set_tags([1] * len(substrate) + [2] * len(film))
11
+ distance = calculate_average_interlayer_distance(interface_atoms, 1, 2)
12
+ assert np.isclose(distance, 4.0725)
@@ -0,0 +1,11 @@
1
+ import numpy as np
2
+ from ase.build import bulk
3
+ from ase.calculators import emt
4
+ from mat3ra.made.tools.calculate import calculate_total_energy
5
+
6
+
7
+ def test_calculate_total_energy():
8
+ atoms = bulk("C", cubic=True)
9
+ calculator = emt.EMT()
10
+ energy = calculate_total_energy(atoms, calculator)
11
+ assert np.isclose(energy, 1.3612647524769237)
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+ from ase import Atoms
3
+ from ase.build import bulk
4
+ from mat3ra.made.material import Material
5
+ from mat3ra.made.tools.convert import from_ase, from_poscar, from_pymatgen, to_ase, to_poscar, to_pymatgen
6
+ from pymatgen.core.structure import Element, Lattice, Structure
7
+
8
+ PYMATGEN_LATTICE = Lattice.from_parameters(a=3.84, b=3.84, c=3.84, alpha=120, beta=90, gamma=60)
9
+ PYMATGEN_STRUCTURE = Structure(PYMATGEN_LATTICE, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]])
10
+
11
+ POSCAR_CONTENT = """Si2
12
+ 1.0
13
+ 3.3489202364344242 0.0000000000000000 1.9335000000000004
14
+ 1.1163067454781415 3.1573922784475164 1.9335000000000004
15
+ 0.0000000000000000 0.0000000000000000 3.8670000000000000
16
+ Si
17
+ 2
18
+ direct
19
+ 0.0000000000000000 0.0000000000000000 0.0000000000000000 Si
20
+ 0.2500000000000000 0.2500000000000000 0.2500000000000000 Si
21
+ """
22
+
23
+
24
+ def test_to_pymatgen():
25
+ material = Material.create(Material.default_config)
26
+ structure = to_pymatgen(material)
27
+ assert isinstance(structure, Structure)
28
+ assert structure.lattice == Lattice.from_parameters(3.867, 3.867, 3.867, 60, 60, 60)
29
+ assert structure.species == [Element("Si"), Element("Si")]
30
+ assert (structure.frac_coords == [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]]).all()
31
+
32
+
33
+ def test_from_pymatgen():
34
+ material_data = from_pymatgen(PYMATGEN_STRUCTURE)
35
+ assert material_data["lattice"]["a"] == 3.84
36
+ assert material_data["lattice"]["alpha"] == 120
37
+
38
+
39
+ def test_to_poscar():
40
+ material = Material.create(Material.default_config)
41
+ poscar = to_poscar(material)
42
+ assert poscar == POSCAR_CONTENT
43
+
44
+
45
+ def test_from_poscar():
46
+ material_data = from_poscar(POSCAR_CONTENT)
47
+ assert material_data["lattice"]["a"] == 3.867
48
+ assert material_data["lattice"]["alpha"] == 60
49
+
50
+
51
+ def test_to_ase():
52
+ material = Material.create(Material.default_config)
53
+ ase_atoms = to_ase(material)
54
+ assert isinstance(ase_atoms, Atoms)
55
+ assert np.allclose(
56
+ ase_atoms.get_cell().tolist(),
57
+ [
58
+ [3.3489202364344242, 0.0, 1.9335000000000004],
59
+ [1.1163067454781415, 3.1573922784475164, 1.9335000000000004],
60
+ [0.0, 0.0, 3.8670000000000000],
61
+ ],
62
+ )
63
+ assert ase_atoms.get_chemical_symbols() == ["Si", "Si"]
64
+ assert np.allclose(ase_atoms.get_scaled_positions().tolist(), [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]])
65
+
66
+
67
+ def test_from_ase():
68
+ ase_atoms = bulk("Si")
69
+ material_data = from_ase(ase_atoms)
70
+ assert material_data["lattice"]["a"] == 3.839589822
71
+ assert material_data["lattice"]["alpha"] == 60
@@ -0,0 +1,11 @@
1
+ from ase.build import bulk
2
+ from mat3ra.made.tools.modify import filter_by_label
3
+
4
+
5
+ def test_filter_by_label():
6
+ substrate = bulk("Si", cubic=True)
7
+ film = bulk("Cu", cubic=True)
8
+ interface_atoms = substrate + film
9
+ interface_atoms.set_tags([1] * len(substrate) + [2] * len(film))
10
+ film_extracted = filter_by_label(interface_atoms, 2)
11
+ assert (film.symbols == film_extracted.symbols).all()
@@ -1,44 +0,0 @@
1
- from typing import Any, Dict, Union
2
-
3
- from mat3ra.made.material import Material
4
- from pymatgen.core.structure import Lattice, Structure
5
-
6
-
7
- def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> Structure:
8
- """
9
- Converts material object in ESSE format to a pymatgen Structure object.
10
-
11
- Args:
12
- material_data (dict): A dictionary containing the material information in ESSE format.
13
-
14
- Returns:
15
- Structure: A pymatgen Structure object.
16
- """
17
- material_data = material_or_material_data
18
-
19
- if isinstance(material_or_material_data, Material):
20
- material_data = material_or_material_data.to_json()
21
-
22
- lattice_params = material_data["lattice"]
23
- a = lattice_params["a"]
24
- b = lattice_params["b"]
25
- c = lattice_params["c"]
26
- alpha = lattice_params["alpha"]
27
- beta = lattice_params["beta"]
28
- gamma = lattice_params["gamma"]
29
-
30
- # Create a Lattice from parameters
31
- lattice = Lattice.from_parameters(a, b, c, alpha, beta, gamma)
32
-
33
- # Extract the basis information
34
- basis = material_data["basis"]
35
- elements = [element["value"] for element in basis["elements"]]
36
- coordinates = [coord["value"] for coord in basis["coordinates"]]
37
-
38
- # Assuming that the basis units are fractional since it's a crystal basis
39
- coords_are_cartesian = "units" in basis and basis["units"].lower() == "angstrom"
40
-
41
- # Create the Structure
42
- structure = Structure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian)
43
-
44
- return structure
@@ -1,12 +0,0 @@
1
- from mat3ra.made.material import Material
2
- from mat3ra.made.tools.material import to_pymatgen
3
- from pymatgen.core.structure import Element, Lattice, Structure
4
-
5
-
6
- def test_to_pymatgen():
7
- material = Material.create(Material.default_config)
8
- structure = to_pymatgen(material)
9
- assert isinstance(structure, Structure)
10
- assert structure.lattice == Lattice.from_parameters(3.867, 3.867, 3.867, 60, 60, 60)
11
- assert structure.species == [Element("Si"), Element("Si")]
12
- assert (structure.frac_coords == [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]]).all()