@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 +75 -35
- package/package.json +1 -1
- package/pyproject.toml +13 -11
- package/src/py/mat3ra/made/tools/analyze.py +41 -0
- package/src/py/mat3ra/made/tools/calculate.py +20 -0
- package/src/py/mat3ra/made/tools/convert.py +186 -0
- package/src/py/mat3ra/made/tools/modify.py +20 -0
- package/tests/py/unit/test_tools_analyze.py +12 -0
- package/tests/py/unit/test_tools_calculate.py +11 -0
- package/tests/py/unit/test_tools_convert.py +71 -0
- package/tests/py/unit/test_tools_modify.py +11 -0
- package/src/py/mat3ra/made/tools/material.py +0 -44
- package/tests/py/unit/test_tools_material.py +0 -12
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
|
-
|
|
8
|
+
## 1. Overview
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
##
|
|
12
|
+
## 2. Installation
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
42
|
+
# B/c of https://github.com/binary-husky/gpt_academic/issues/1237
|
|
38
43
|
"gradio",
|
|
39
|
-
|
|
40
|
-
|
|
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[
|
|
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()
|