@mat3ra/made 2025.11.4-0 → 2025.11.22-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/dist/js/materialMixin.d.ts +8 -0
- package/dist/js/materialMixin.js +6 -0
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/js/materialMixin.ts +7 -0
- package/src/py/mat3ra/made/tools/analyze/basis/__init__.py +1 -2
- package/src/py/mat3ra/made/tools/analyze/basis/analyzer.py +2 -34
- package/src/py/mat3ra/made/tools/analyze/enums.py +33 -0
- package/src/py/mat3ra/made/tools/analyze/fingerprint/__init__.py +4 -0
- package/src/py/mat3ra/made/tools/analyze/fingerprint/layers/__init__.py +0 -0
- package/src/py/mat3ra/made/tools/analyze/fingerprint/layers/layered_fingerprint_along_axis.py +84 -0
- package/src/py/mat3ra/made/tools/analyze/fingerprint/layers/unique_element_string_per_layer.py +9 -0
- package/src/py/mat3ra/made/tools/analyze/lattice/analyzer.py +18 -14
- package/src/py/mat3ra/made/tools/analyze/lattice_swap_analyzer.py +104 -0
- package/src/py/mat3ra/made/tools/operations/core/unary.py +8 -7
- package/src/py/mat3ra/made/tools/operations/reusable/unary.py +25 -1
- package/tests/py/unit/fixtures/interface/gaas_dia.py +837 -0
- package/tests/py/unit/fixtures/interface/zsl.py +5 -5
- package/tests/py/unit/test_operations.py +22 -0
- package/tests/py/unit/test_tools_analyze_basis.py +67 -36
- package/tests/py/unit/test_tools_analyze_lattice.py +39 -8
- package/tests/py/unit/test_tools_analyze_swap.py +74 -0
- package/tests/py/unit/test_tools_modify.py +19 -5
- package/src/py/mat3ra/made/tools/analyze/basis/fingerprint.py +0 -65
|
@@ -108,6 +108,14 @@ export declare function materialMixin<T extends Base = Base>(item: T): {
|
|
|
108
108
|
* Calculates hash from basis and lattice as above + scales lattice properties to make lattice.a = 1
|
|
109
109
|
*/
|
|
110
110
|
readonly scaledHash: string;
|
|
111
|
+
external: {
|
|
112
|
+
id: string | number;
|
|
113
|
+
source: string;
|
|
114
|
+
origin: boolean;
|
|
115
|
+
data?: {} | undefined;
|
|
116
|
+
doi?: string | undefined;
|
|
117
|
+
url?: string | undefined;
|
|
118
|
+
} | undefined;
|
|
111
119
|
/**
|
|
112
120
|
* Converts basis to crystal/fractional coordinates.
|
|
113
121
|
*/
|
package/dist/js/materialMixin.js
CHANGED
|
@@ -232,6 +232,12 @@ function materialMixin(item) {
|
|
|
232
232
|
get scaledHash() {
|
|
233
233
|
return this.calculateHash("", true);
|
|
234
234
|
},
|
|
235
|
+
get external() {
|
|
236
|
+
return item.prop("external");
|
|
237
|
+
},
|
|
238
|
+
set external(external) {
|
|
239
|
+
item.setProp("external", external);
|
|
240
|
+
},
|
|
235
241
|
/**
|
|
236
242
|
* Converts basis to crystal/fractional coordinates.
|
|
237
243
|
*/
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/js/materialMixin.ts
CHANGED
|
@@ -290,6 +290,13 @@ export function materialMixin<T extends Base = Base>(item: T) {
|
|
|
290
290
|
return this.calculateHash("", true);
|
|
291
291
|
},
|
|
292
292
|
|
|
293
|
+
get external() {
|
|
294
|
+
return item.prop<MaterialSchema["external"]>("external");
|
|
295
|
+
},
|
|
296
|
+
set external(external: MaterialSchema["external"]) {
|
|
297
|
+
item.setProp("external", external);
|
|
298
|
+
},
|
|
299
|
+
|
|
293
300
|
/**
|
|
294
301
|
* Converts basis to crystal/fractional coordinates.
|
|
295
302
|
*/
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
|
|
2
2
|
from mat3ra.made.utils import AXIS_TO_INDEX_MAP
|
|
3
3
|
|
|
4
|
-
from ...build_components.metadata import MaterialWithBuildMetadata
|
|
5
4
|
from .. import BaseMaterialAnalyzer
|
|
6
|
-
from
|
|
5
|
+
from ..fingerprint import LayeredFingerprintAlongAxis, UniqueElementStringsPerLayer
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class BasisMaterialAnalyzer(BaseMaterialAnalyzer):
|
|
@@ -43,40 +42,9 @@ class BasisMaterialAnalyzer(BaseMaterialAnalyzer):
|
|
|
43
42
|
layer_elements.append(elements[i])
|
|
44
43
|
|
|
45
44
|
unique_elements = sorted(list(set(layer_elements))) if layer_elements else []
|
|
46
|
-
layer =
|
|
45
|
+
layer = UniqueElementStringsPerLayer(min_coord=layer_min, max_coord=layer_max, elements=unique_elements)
|
|
47
46
|
fingerprint.layers.append(layer)
|
|
48
47
|
|
|
49
48
|
current_coord += layer_thickness
|
|
50
49
|
|
|
51
50
|
return fingerprint
|
|
52
|
-
|
|
53
|
-
def is_orientation_flipped(
|
|
54
|
-
self, original_material: MaterialWithBuildMetadata, layer_thickness: float = 1.0
|
|
55
|
-
) -> bool:
|
|
56
|
-
"""
|
|
57
|
-
Detect if the material orientation is flipped compared to the original.
|
|
58
|
-
Uses Jaccard similarity to compare fingerprints in normal and flipped orientations.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
original_material: The original material before primitivization
|
|
62
|
-
layer_thickness: Thickness of layers for fingerprint comparison
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
bool: True if orientation is flipped, False otherwise
|
|
66
|
-
"""
|
|
67
|
-
original_analyzer = BasisMaterialAnalyzer(material=original_material)
|
|
68
|
-
original_fingerprint = original_analyzer.get_layer_fingerprint(layer_thickness)
|
|
69
|
-
current_fingerprint = self.get_layer_fingerprint(layer_thickness)
|
|
70
|
-
|
|
71
|
-
normal_score = original_fingerprint.get_similarity_score(current_fingerprint)
|
|
72
|
-
flipped_score = original_fingerprint.get_similarity_score(self._reverse_fingerprint(current_fingerprint))
|
|
73
|
-
|
|
74
|
-
# If flipped orientation has significantly higher similarity, material is flipped
|
|
75
|
-
threshold = 0.1
|
|
76
|
-
return flipped_score > normal_score + threshold
|
|
77
|
-
|
|
78
|
-
def _reverse_fingerprint(self, fingerprint: LayeredFingerprintAlongAxis) -> LayeredFingerprintAlongAxis:
|
|
79
|
-
reversed_layers = list(reversed(fingerprint.layers))
|
|
80
|
-
return LayeredFingerprintAlongAxis(
|
|
81
|
-
layers=reversed_layers, axis=fingerprint.axis, layer_thickness=fingerprint.layer_thickness
|
|
82
|
-
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
POSSIBLE_TRANSFORMATION_MATRICES = [
|
|
2
|
+
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], # Identity - no transformation
|
|
3
|
+
# Direct swaps: new[i] = old[j]
|
|
4
|
+
[[0, 1, 0], [1, 0, 0], [0, 0, 1]],
|
|
5
|
+
[[0, 0, 1], [0, 1, 0], [1, 0, 0]],
|
|
6
|
+
[[1, 0, 0], [0, 0, 1], [0, 1, 0]],
|
|
7
|
+
# Swaps with sign flips
|
|
8
|
+
[[0, 0, 1], [1, 0, 0], [0, -1, 0]],
|
|
9
|
+
[[0, 0, -1], [1, 0, 0], [0, 1, 0]],
|
|
10
|
+
[[0, 1, 0], [0, 0, 1], [-1, 0, 0]],
|
|
11
|
+
[[0, -1, 0], [0, 0, 1], [1, 0, 0]],
|
|
12
|
+
# Inverted swaps
|
|
13
|
+
[[0, -1, 0], [-1, 0, 0], [0, 0, 1]],
|
|
14
|
+
[[0, 0, -1], [0, 1, 0], [-1, 0, 0]],
|
|
15
|
+
[[1, 0, 0], [0, 0, -1], [0, -1, 0]],
|
|
16
|
+
# Rotations around x-axis: -90 and +90 degrees
|
|
17
|
+
[[1, 0, 0], [0, 0, 1], [0, -1, 0]], # -90° around x
|
|
18
|
+
[[1, 0, 0], [0, 0, -1], [0, 1, 0]], # +90° around x
|
|
19
|
+
# Rotations around y-axis: -90 and +90 degrees
|
|
20
|
+
[[0, 0, -1], [0, 1, 0], [1, 0, 0]], # -90° around y
|
|
21
|
+
[[0, 0, 1], [0, 1, 0], [-1, 0, 0]], # +90° around y
|
|
22
|
+
# Rotations around z-axis: -90 and +90 degrees
|
|
23
|
+
[[0, 1, 0], [-1, 0, 0], [0, 0, 1]], # -90° around z
|
|
24
|
+
[[0, -1, 0], [1, 0, 0], [0, 0, 1]], # +90° around z
|
|
25
|
+
# 180° rotations around axes
|
|
26
|
+
[[1, 0, 0], [0, -1, 0], [0, 0, -1]], # 180° around x
|
|
27
|
+
[[-1, 0, 0], [0, 1, 0], [0, 0, -1]], # 180° around y
|
|
28
|
+
[[-1, 0, 0], [0, -1, 0], [0, 0, 1]], # 180° around z
|
|
29
|
+
# Mirrors (reflections)
|
|
30
|
+
[[1, 0, 0], [0, 1, 0], [0, 0, -1]], # Mirror along z
|
|
31
|
+
[[1, 0, 0], [0, -1, 0], [0, 0, 1]], # Mirror along y
|
|
32
|
+
[[-1, 0, 0], [0, 1, 0], [0, 0, 1]], # Mirror along x
|
|
33
|
+
]
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from mat3ra.made.tools.analyze.fingerprint.layers.unique_element_string_per_layer import UniqueElementStringsPerLayer
|
|
2
|
+
from mat3ra.made.tools.analyze.fingerprint.layers.layered_fingerprint_along_axis import LayeredFingerprintAlongAxis
|
|
3
|
+
|
|
4
|
+
__all__ = ["UniqueElementStringsPerLayer", "LayeredFingerprintAlongAxis"]
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from itertools import cycle, islice
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from mat3ra.utils.array import jaccard_similarity_for_strings
|
|
5
|
+
from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from mat3ra.made.tools.analyze.fingerprint.layers.unique_element_string_per_layer import UniqueElementStringsPerLayer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LayeredFingerprintAlongAxis(BaseModel):
|
|
12
|
+
layers: List[UniqueElementStringsPerLayer] = Field(default_factory=list, description="List of layer fingerprints")
|
|
13
|
+
axis: AxisEnum = Field(default=AxisEnum.z, description="Axis along which the fingerprint is computed")
|
|
14
|
+
layer_thickness: float = Field(default=1.0, gt=0, description="Thickness of each layer in Angstroms")
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def non_empty_layers(self) -> List[UniqueElementStringsPerLayer]:
|
|
18
|
+
return [layer for layer in self.layers if layer.elements]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def element_sequence(self) -> List[List[str]]:
|
|
22
|
+
return [layer.elements for layer in self.layers]
|
|
23
|
+
|
|
24
|
+
def get_similarity_score(self, other: "LayeredFingerprintAlongAxis") -> float:
|
|
25
|
+
"""
|
|
26
|
+
Calculate Jaccard similarity score between this and another fingerprint.
|
|
27
|
+
|
|
28
|
+
The Jaccard similarity coefficient measures the similarity between two sets by comparing
|
|
29
|
+
the size of their intersection to the size of their union: J(A, B) = |A ∩ B| / |A ∪ B|.
|
|
30
|
+
In this implementation, we compare the sets of chemical elements in corresponding layers
|
|
31
|
+
between two fingerprints. For example, if layer 1 contains {Si, O} and the corresponding
|
|
32
|
+
layer contains {Si, Ge}, the Jaccard score is 1/3 (one common element divided by three
|
|
33
|
+
unique elements total). The final score is the average across all layers, providing a
|
|
34
|
+
measure of compositional similarity along the axis (0.0 = completely different,
|
|
35
|
+
1.0 = identical composition in all layers).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
other: Another LayeredFingerprintAlongAxis to compare with
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
float: Average Jaccard similarity score (0.0 to 1.0). Returns 0.0 if the number of layers doesn't match.
|
|
42
|
+
"""
|
|
43
|
+
if not self.layers or not other.layers:
|
|
44
|
+
return 0.0
|
|
45
|
+
if len(self.layers) != len(other.layers):
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
seq1, seq2 = self.element_sequence, other.element_sequence
|
|
49
|
+
return sum(jaccard_similarity_for_strings(a, b) for a, b in zip(seq1, seq2)) / len(seq1)
|
|
50
|
+
|
|
51
|
+
def get_similarity_score_ignore_periodicity(self, other: "LayeredFingerprintAlongAxis") -> float:
|
|
52
|
+
"""
|
|
53
|
+
Calculate Jaccard similarity score ignoring periodicity differences.
|
|
54
|
+
|
|
55
|
+
Handles cases where one fingerprint is a periodic repetition of another.
|
|
56
|
+
For example, if self has 6 layers and other has 2 layers, this method checks if
|
|
57
|
+
self.layers is a 3× repetition of other.layers (i.e., layers 0–1 match other,
|
|
58
|
+
layers 2–3 match other, etc.).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
other: Another LayeredFingerprintAlongAxis to compare with
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
float: Average Jaccard similarity score (0.0 to 1.0). Returns 0.0 if the number
|
|
65
|
+
of layers is not a multiple relationship or if fingerprints don't match.
|
|
66
|
+
"""
|
|
67
|
+
if not self.layers or not other.layers:
|
|
68
|
+
return 0.0
|
|
69
|
+
|
|
70
|
+
len_a, len_b = len(self.layers), len(other.layers)
|
|
71
|
+
if len_a == len_b:
|
|
72
|
+
return self.get_similarity_score(other)
|
|
73
|
+
|
|
74
|
+
# Identify shorter/longer and ensure multiplicity
|
|
75
|
+
if len_a < len_b:
|
|
76
|
+
short_seq, long_seq = self.element_sequence, other.element_sequence
|
|
77
|
+
else:
|
|
78
|
+
short_seq, long_seq = other.element_sequence, self.element_sequence
|
|
79
|
+
|
|
80
|
+
if len(long_seq) % len(short_seq) != 0:
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
cycled_short = islice(cycle(short_seq), len(long_seq))
|
|
84
|
+
return sum(jaccard_similarity_for_strings(a, b) for a, b in zip(cycled_short, long_seq)) / len(short_seq)
|
package/src/py/mat3ra/made/tools/analyze/fingerprint/layers/unique_element_string_per_layer.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UniqueElementStringsPerLayer(BaseModel):
|
|
7
|
+
min_coord: float = Field(..., description="Minimum coordinate value for the layer")
|
|
8
|
+
max_coord: float = Field(..., description="Maximum coordinate value for the layer")
|
|
9
|
+
elements: List[str] = Field(default_factory=list, description="Sorted unique chemical elements in the layer")
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from .. import BaseMaterialAnalyzer
|
|
2
|
+
from ..lattice_swap_analyzer import MaterialLatticeSwapAnalyzer
|
|
2
3
|
from ...build_components.metadata import MaterialWithBuildMetadata
|
|
3
4
|
from ...convert import from_pymatgen, to_pymatgen
|
|
4
|
-
from ...operations.core.unary import rotate
|
|
5
5
|
from ...third_party import PymatgenSpacegroupAnalyzer
|
|
6
|
-
from
|
|
7
|
-
from ..basis import BasisMaterialAnalyzer
|
|
6
|
+
from ....lattice import LatticeTypeEnum
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class LatticeMaterialAnalyzer(BaseMaterialAnalyzer):
|
|
@@ -88,15 +87,20 @@ class LatticeMaterialAnalyzer(BaseMaterialAnalyzer):
|
|
|
88
87
|
)
|
|
89
88
|
|
|
90
89
|
def get_material_with_primitive_lattice_standard(
|
|
91
|
-
self,
|
|
90
|
+
self,
|
|
91
|
+
return_original_if_not_reduced: bool = False,
|
|
92
|
+
keep_orientation: bool = True,
|
|
93
|
+
layer_thickness: float = 1.0,
|
|
94
|
+
rotation_detection_threshold: float = 0.05,
|
|
92
95
|
) -> MaterialWithBuildMetadata:
|
|
93
96
|
"""
|
|
94
|
-
Get material with primitive lattice
|
|
97
|
+
Get material with primitive lattice standardized according to IUCr conventions.
|
|
95
98
|
|
|
96
99
|
Args:
|
|
97
100
|
return_original_if_not_reduced: If True, return original material when no reduction occurs
|
|
98
|
-
keep_orientation: If True,
|
|
99
|
-
layer_thickness:
|
|
101
|
+
keep_orientation: If True, detect and reverse lattice parameter swaps to preserve original orientation
|
|
102
|
+
layer_thickness: Unused (kept for compatibility)
|
|
103
|
+
rotation_detection_threshold: Unused (kept for compatibility)
|
|
100
104
|
|
|
101
105
|
Returns:
|
|
102
106
|
MaterialWithBuildMetadata: Material with primitive lattice
|
|
@@ -110,11 +114,11 @@ class LatticeMaterialAnalyzer(BaseMaterialAnalyzer):
|
|
|
110
114
|
return self.material
|
|
111
115
|
|
|
112
116
|
if keep_orientation:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
swap_analyzer = MaterialLatticeSwapAnalyzer(material=material_with_primitive_lattice)
|
|
118
|
+
material_with_primitive_lattice = swap_analyzer.get_corrected_material(
|
|
119
|
+
self.material, layer_thickness=layer_thickness, threshold=rotation_detection_threshold
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
material_with_primitive_lattice.metadata = self.material.metadata
|
|
119
123
|
|
|
120
124
|
return material_with_primitive_lattice
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from mat3ra.made.material import Material
|
|
8
|
+
from .basis import BasisMaterialAnalyzer
|
|
9
|
+
from .enums import POSSIBLE_TRANSFORMATION_MATRICES
|
|
10
|
+
from ..build_components.metadata.material_with_build_metadata import MaterialWithBuildMetadata
|
|
11
|
+
from ..operations.reusable.unary import transform_material_by_matrix
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LatticeSwapDetectionResult(BaseModel):
|
|
15
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
16
|
+
is_swapped: bool = Field(..., description="Whether a lattice swap was detected")
|
|
17
|
+
permutation: Matrix3x3Schema = Field(..., description="Transformation matrix representing the swap")
|
|
18
|
+
confidence: float = Field(..., description="Confidence score (0.0 to 1.0)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MaterialLatticeSwapAnalyzer(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Analyzer to detect lattice vector swaps/permutations between materials.
|
|
24
|
+
|
|
25
|
+
This detects when lattice vectors have been reoriented (e.g., a->a, b->c, c->-b)
|
|
26
|
+
rather than the basis being rotated within the lattice.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
material: Union[Material, MaterialWithBuildMetadata]
|
|
30
|
+
tolerance: float = 0.01
|
|
31
|
+
|
|
32
|
+
def _compute_transformation_score(self, matrix: np.ndarray, target_fingerprint) -> float:
|
|
33
|
+
transformed_material = transform_material_by_matrix(self.material, matrix)
|
|
34
|
+
new_analyzer = BasisMaterialAnalyzer(material=transformed_material)
|
|
35
|
+
new_fingerprint = new_analyzer.get_layer_fingerprint(target_fingerprint.layer_thickness)
|
|
36
|
+
return target_fingerprint.get_similarity_score(new_fingerprint)
|
|
37
|
+
|
|
38
|
+
def _create_detection_result(self, matrix: np.ndarray, score: float) -> LatticeSwapDetectionResult:
|
|
39
|
+
is_identity = np.allclose(matrix, np.eye(3), atol=self.tolerance)
|
|
40
|
+
return LatticeSwapDetectionResult(
|
|
41
|
+
is_swapped=not is_identity,
|
|
42
|
+
permutation=Matrix3x3Schema(root=matrix.tolist()),
|
|
43
|
+
confidence=score,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def detect_swap_from_original(
|
|
47
|
+
self, original_material: MaterialWithBuildMetadata, layer_thickness: float = 1.0, threshold: float = 0.1
|
|
48
|
+
) -> LatticeSwapDetectionResult:
|
|
49
|
+
"""
|
|
50
|
+
Detect lattice swap from the original material.
|
|
51
|
+
|
|
52
|
+
We apply every transformation matrix to lattice and basis, compute the fingerprint along z, then compare
|
|
53
|
+
to the original material's fingerprint. The best matching transformation (if above threshold) is considered
|
|
54
|
+
a detected swap.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
original_material: The original material before transformation
|
|
59
|
+
layer_thickness: Thickness of layers for fingerprint comparison
|
|
60
|
+
threshold: Minimum improvement threshold to consider a swap detected
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
LatticeSwapDetectionResult: Swap detection results with permutation and new lattice
|
|
64
|
+
"""
|
|
65
|
+
target_analyzer = BasisMaterialAnalyzer(material=original_material)
|
|
66
|
+
target_fingerprint = target_analyzer.get_layer_fingerprint(layer_thickness)
|
|
67
|
+
possible_matrices = list(map(np.array, POSSIBLE_TRANSFORMATION_MATRICES))
|
|
68
|
+
|
|
69
|
+
best_match = None
|
|
70
|
+
max_score = 0
|
|
71
|
+
|
|
72
|
+
for matrix in possible_matrices:
|
|
73
|
+
score = self._compute_transformation_score(matrix, target_fingerprint)
|
|
74
|
+
if score > max_score:
|
|
75
|
+
best_match = self._create_detection_result(matrix, score)
|
|
76
|
+
max_score = score
|
|
77
|
+
|
|
78
|
+
if best_match and best_match.is_swapped and best_match.confidence >= threshold:
|
|
79
|
+
return best_match
|
|
80
|
+
return best_match
|
|
81
|
+
|
|
82
|
+
def get_corrected_material(
|
|
83
|
+
self,
|
|
84
|
+
target: MaterialWithBuildMetadata,
|
|
85
|
+
layer_thickness: float = 1.0,
|
|
86
|
+
threshold: float = 0.1,
|
|
87
|
+
) -> MaterialWithBuildMetadata:
|
|
88
|
+
"""
|
|
89
|
+
Correct the material's lattice to match the target material's orientation if a swap is detected.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
target: The target material to match
|
|
93
|
+
layer_thickness: Thickness of layers for fingerprint comparison
|
|
94
|
+
threshold: Minimum improvement threshold to consider a swap detected
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
MaterialWithBuildMetadata: Corrected material with lattice matching the target orientation
|
|
98
|
+
"""
|
|
99
|
+
swap_info = self.detect_swap_from_original(target, layer_thickness, threshold)
|
|
100
|
+
if swap_info.is_swapped:
|
|
101
|
+
matrix_list = [list(row.root) if hasattr(row, "root") else row for row in swap_info.permutation.root]
|
|
102
|
+
matrix_array = np.array(matrix_list)
|
|
103
|
+
return transform_material_by_matrix(self.material, matrix_array)
|
|
104
|
+
return self.material
|
|
@@ -101,23 +101,24 @@ def perturb(
|
|
|
101
101
|
return new_material
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def rotate(material: Material, axis: List[int], angle: float, wrap: bool = True
|
|
104
|
+
def rotate(material: Material, axis: List[int], angle: float, wrap: bool = True) -> Material:
|
|
105
105
|
"""
|
|
106
|
-
Rotate the
|
|
106
|
+
Rotate the basis of the material relative to the lattice.
|
|
107
|
+
This operation breaks symmetry and does not modify lattice vectors.
|
|
107
108
|
|
|
108
109
|
Args:
|
|
109
110
|
material (Material): The material to rotate.
|
|
110
111
|
axis (List[int]): The axis to rotate around, expressed as [x, y, z].
|
|
111
112
|
angle (float): The angle of rotation in degrees.
|
|
112
113
|
wrap (bool): Whether to wrap the material to the unit cell.
|
|
113
|
-
rotate_cell (bool): Whether to rotate the cell.
|
|
114
114
|
Returns:
|
|
115
115
|
Atoms: The rotated material.
|
|
116
116
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
atoms
|
|
117
|
+
new_material = material.clone()
|
|
118
|
+
original_is_in_cartesian_units = new_material.basis.is_in_cartesian_units
|
|
119
|
+
new_material.to_crystal()
|
|
120
|
+
atoms = to_ase(new_material)
|
|
121
|
+
atoms.rotate(v=axis, a=angle, center="COU", rotate_cell=False)
|
|
121
122
|
if wrap:
|
|
122
123
|
atoms.wrap()
|
|
123
124
|
new_material = MaterialWithBuildMetadata.create(from_ase(atoms))
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
|
|
3
|
-
from mat3ra.made.material import Material
|
|
4
3
|
|
|
4
|
+
from mat3ra.made.material import Material
|
|
5
5
|
from ..core.unary import strain
|
|
6
|
+
from ...modify import wrap_to_unit_cell
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def transform_material_by_matrix(material: Material, matrix: np.ndarray) -> Material:
|
|
10
|
+
"""
|
|
11
|
+
Transforms a material by applying a transformation matrix to lattice vectors and coordinates.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
material: The material to be transformed.
|
|
15
|
+
matrix: The 3x3 transformation matrix as a numpy array.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A new material instance with transformed lattice and coordinates, wrapped to unit cell.
|
|
19
|
+
"""
|
|
20
|
+
current_lattice_vectors = np.array(material.lattice.vector_arrays)
|
|
21
|
+
current_coordinates = material.basis.coordinates.values
|
|
22
|
+
|
|
23
|
+
new_lattice_vectors = (matrix @ current_lattice_vectors.T).tolist()
|
|
24
|
+
new_coordinates = (np.linalg.inv(matrix) @ np.array(current_coordinates).T).T.tolist()
|
|
25
|
+
|
|
26
|
+
transformed_material = material.clone()
|
|
27
|
+
transformed_material.set_lattice_vectors_from_array(new_lattice_vectors)
|
|
28
|
+
transformed_material.basis.coordinates.values = new_coordinates
|
|
29
|
+
return wrap_to_unit_cell(transformed_material)
|
|
6
30
|
|
|
7
31
|
|
|
8
32
|
def strain_to_match_lattice(material_to_strain: Material, target_material: Material) -> Material:
|