materialite 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 United States Government as represented by the Administrator of the
2
+ # National Aeronautics and Space Administration.  All Rights Reserved.
3
+ #
4
+ # The Materialite platform is licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed
9
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
11
+ # specific language governing permissions and limitations under the License.
12
+
13
+ # Skip isort on this file to avoid circular imports
14
+ # isort: skip_file
15
+
16
+ from .material import Material, Sphere, Superellipsoid, Feature, Box
17
+ from .tensor import (
18
+ Orientation,
19
+ Scalar,
20
+ Vector,
21
+ Order2Tensor,
22
+ Order2SymmetricTensor,
23
+ Order4SymmetricTensor,
24
+ )
25
+ from .slip_system import SlipSystem
26
+ from .json_operations import read_from_json, write_to_json
27
+ from .get_ipf_colors import get_ipf_colors, get_ipf, add_ipf_colors_field
28
+ from .importers import import_dream3d, import_evpfft, import_spparks, import_vgstudio
@@ -0,0 +1,118 @@
1
+ # Copyright 2025 United States Government as represented by the Administrator of the
2
+ # National Aeronautics and Space Administration.  All Rights Reserved.
3
+ #
4
+ # The Materialite platform is licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed
9
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
11
+ # specific language governing permissions and limitations under the License.
12
+
13
+ import numpy as np
14
+
15
+
16
+ # Not particularly liking calling this the natural basis but it is here for now
17
+ def natural_basis():
18
+ r2 = 1 / np.sqrt(2)
19
+ r3 = 1 / np.sqrt(3)
20
+ r6 = 1 / np.sqrt(6)
21
+ return np.array(
22
+ [
23
+ r3 * np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]),
24
+ r6 * np.array([[-1, 0, 0], [0, -1, 0], [0, 0, 2]]),
25
+ r2 * np.array([[-1, 0, 0], [0, 1, 0], [0, 0, 0]]),
26
+ r2 * np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
27
+ r2 * np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
28
+ r2 * np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
29
+ ]
30
+ )
31
+
32
+
33
+ def natural_product_basis():
34
+ return np.einsum("mij, nkl -> mnijkl", natural_basis(), natural_basis())
35
+
36
+
37
+ def mandel_basis():
38
+ r2 = 1 / np.sqrt(2)
39
+ return np.array(
40
+ [
41
+ [[1.0, 0, 0], [0, 0, 0], [0, 0, 0]],
42
+ [[0, 0, 0], [0, 1.0, 0], [0, 0, 0]],
43
+ [[0, 0, 0], [0, 0, 0], [0, 0, 1.0]],
44
+ [[0, 0, 0], [0, 0, r2], [0, r2, 0]],
45
+ [[0, 0, r2], [0, 0, 0], [r2, 0, 0]],
46
+ [[0, r2, 0], [r2, 0, 0], [0, 0, 0]],
47
+ ]
48
+ )
49
+
50
+
51
+ def mandel_product_basis():
52
+ return np.einsum("mij, nkl -> mnijkl", mandel_basis(), mandel_basis())
53
+
54
+
55
+ def mises_basis():
56
+ r2 = 1 / np.sqrt(2)
57
+ r3 = 1 / np.sqrt(3)
58
+ r6 = 1 / np.sqrt(6)
59
+ return np.array(
60
+ [
61
+ r2 * np.array([[-1, 0, 0], [0, 1, 0], [0, 0, 0]]),
62
+ r6 * np.array([[-1, 0, 0], [0, -1, 0], [0, 0, 2]]),
63
+ r2 * np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
64
+ r2 * np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
65
+ r2 * np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
66
+ r3 * np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]),
67
+ ]
68
+ )
69
+
70
+
71
+ def mises_product_basis():
72
+ return np.einsum("mij, nkl -> mnijkl", mises_basis(), mises_basis())
73
+
74
+
75
+ def strain_voigt_basis():
76
+ return np.array(
77
+ [
78
+ np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]),
79
+ np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]),
80
+ np.array([[0, 0, 0], [0, 0, 0], [0, 0, 1]]),
81
+ 0.5 * np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]]),
82
+ 0.5 * np.array([[0, 0, 1], [0, 0, 0], [1, 0, 0]]),
83
+ 0.5 * np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]),
84
+ ]
85
+ )
86
+
87
+
88
+ def stress_voigt_basis():
89
+ return np.array(
90
+ [
91
+ [[1.0, 0, 0], [0, 0, 0], [0, 0, 0]],
92
+ [[0, 0, 0], [0, 1.0, 0], [0, 0, 0]],
93
+ [[0, 0, 0], [0, 0, 0], [0, 0, 1.0]],
94
+ [[0, 0, 0], [0, 0, 1.0], [0, 1.0, 0]],
95
+ [[0, 0, 1.0], [0, 0, 0], [1.0, 0, 0]],
96
+ [[0, 1.0, 0], [1.0, 0, 0], [0, 0, 0]],
97
+ ]
98
+ )
99
+
100
+
101
+ def strain_voigt_dual_basis():
102
+ return stress_voigt_basis()
103
+
104
+
105
+ def stress_voigt_dual_basis():
106
+ return strain_voigt_basis()
107
+
108
+
109
+ def voigt_product_basis():
110
+ return np.einsum(
111
+ "mij, nkl -> mnijkl", stress_voigt_basis(), strain_voigt_dual_basis()
112
+ )
113
+
114
+
115
+ def voigt_dual_product_basis():
116
+ return np.einsum(
117
+ "mij, nkl -> mnijkl", stress_voigt_dual_basis(), strain_voigt_basis()
118
+ )
@@ -0,0 +1,199 @@
1
+ # Copyright 2025 United States Government as represented by the Administrator of the
2
+ # National Aeronautics and Space Administration.  All Rights Reserved.
3
+ #
4
+ # The Materialite platform is licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed
9
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
11
+ # specific language governing permissions and limitations under the License.
12
+
13
+ import numpy as np
14
+ from materialite.tensor import Vector
15
+ from numpy.linalg import norm
16
+
17
+
18
+ def add_ipf_colors_field(
19
+ material,
20
+ orientation_label="orientation",
21
+ specimen_frame_direction=Vector([0, 0, 1]),
22
+ unit_cell="cubic",
23
+ ipf_color_label="ipf_color",
24
+ ):
25
+ orientations = material.extract(orientation_label)
26
+ ipf_colors = get_ipf_colors(specimen_frame_direction, orientations, unit_cell)
27
+ return material.create_fields({ipf_color_label: ipf_colors.tolist()})
28
+
29
+
30
+ def get_ipf_colors(specimen_frame_direction, orientations, unit_cell):
31
+ specimen_frame_direction = Vector(specimen_frame_direction)
32
+ direction = specimen_frame_direction / specimen_frame_direction.norm
33
+ crystal_directions = direction.to_crystal_frame(orientations)
34
+ ipf_points = _convert_to_fundamental_region(crystal_directions, unit_cell)
35
+ return _get_ipf_colors_from_fundamental_points(ipf_points, unit_cell)
36
+
37
+
38
+ def get_ipf(specimen_frame_direction, orientations, unit_cell):
39
+ specimen_frame_direction = Vector(specimen_frame_direction)
40
+ direction = specimen_frame_direction / specimen_frame_direction.norm
41
+ crystal_directions = direction.to_crystal_frame(orientations)
42
+ ipf_points_3D = _convert_to_fundamental_region(crystal_directions, unit_cell)
43
+ ipf_points = _get_stereographic_projection(ipf_points_3D)
44
+ ipf_boundary = _get_ipf_boundary(unit_cell)
45
+ return ipf_points, ipf_boundary
46
+
47
+
48
+ def _convert_to_fundamental_region(directions, unit_cell):
49
+ if unit_cell.lower() in ["cubic", "fcc", "bcc"]:
50
+ p = np.atleast_2d(directions.components)
51
+ p[p[:, 0] < 0, 0] *= -1
52
+ p[p[:, 1] < 0, 1] *= -1
53
+ p[p[:, 2] < 0, 2] *= -1
54
+
55
+ cond = p[:, 0] < p[:, 1]
56
+ p[cond, 0], p[cond, 1] = p[cond, 1], p[cond, 0].copy()
57
+
58
+ cond = p[:, 2] < p[:, 0]
59
+ p[cond, 2], p[cond, 0] = p[cond, 0], p[cond, 2].copy()
60
+
61
+ cond = p[:, 0] < p[:, 1]
62
+ p[cond, 0], p[cond, 1] = p[cond, 1], p[cond, 0].copy()
63
+ elif unit_cell.lower() in ["hcp", "hexagonal"]:
64
+ p = np.atleast_2d(directions.components).copy()
65
+ p[p[:, 0] < 0, 0] *= -1
66
+ p[p[:, 1] < 0, 1] *= -1
67
+ p[p[:, 2] < 0, 2] *= -1
68
+ angles = np.arctan2(p[:, 1], p[:, 0])
69
+ reduce_cond = angles >= np.pi / 3
70
+ c60 = 0.5
71
+ s60 = np.sqrt(3) / 2
72
+ reduce_R = np.array([[c60, s60], [-s60, c60]])
73
+ p[reduce_cond, :2] = np.einsum("ij, bj -> bi", reduce_R, p[reduce_cond, :2])
74
+ angles[reduce_cond] -= np.pi / 3
75
+
76
+ reflect_cond = angles >= np.pi / 6
77
+ reflect_R = np.array([[c60, s60], [s60, -c60]])
78
+ p[reflect_cond, :2] = np.einsum("ij, bj -> bi", reflect_R, p[reflect_cond, :2])
79
+ np.arctan2(p[:, 1], p[:, 0]) * 180 / np.pi
80
+
81
+ return p
82
+
83
+
84
+ def _get_ipf_colors_from_fundamental_points(points, unit_cell):
85
+ if unit_cell.lower() in ["cubic", "fcc", "bcc"]:
86
+ u = points[:, 2] - points[:, 0]
87
+ v = np.sqrt(2) * (points[:, 0] - points[:, 1])
88
+ w = np.sqrt(3) * points[:, 1]
89
+ uvw = np.c_[u, v, w]
90
+ elif unit_cell.lower() in ["hexagonal", "hcp"]:
91
+ u = points[:, 2]
92
+ v = points[:, 0] - np.sqrt(3) * points[:, 1]
93
+ w = 2 * points[:, 1]
94
+ uvw = np.c_[u, v, w]
95
+ return _adjust_colors(uvw)
96
+
97
+
98
+ def _adjust_colors(uvw):
99
+ uvw = uvw / np.max(np.abs(uvw), axis=1, keepdims=True)
100
+ return np.sqrt(uvw)
101
+
102
+
103
+ def _get_stereographic_projection(ipf_points):
104
+ azimuth, elevation = _cartesian_to_spherical(ipf_points)
105
+ P = 1.0 / np.cos(elevation / 2.0)
106
+ x_projected = P * np.sin(elevation / 2) * np.cos(azimuth)
107
+ y_projected = P * np.sin(elevation / 2) * np.sin(azimuth)
108
+ return np.c_[x_projected, y_projected]
109
+
110
+
111
+ def _cartesian_to_spherical(xyz):
112
+ xy = xyz[:, 0] ** 2 + xyz[:, 1] ** 2
113
+ elevation = np.arctan2(np.sqrt(xy), xyz[:, 2])
114
+ azimuth = np.arctan2(xyz[:, 1], xyz[:, 0])
115
+ return azimuth, elevation
116
+
117
+
118
+ def _get_ipf_boundary(unit_cell):
119
+ num_points = 1001
120
+ if unit_cell.lower() in ["cubic", "fcc", "bcc"]:
121
+ great_circle_xz = np.ones(num_points)
122
+ great_circle_points_3D = np.c_[
123
+ great_circle_xz, np.linspace(0, 1, num_points), great_circle_xz
124
+ ]
125
+ great_circle_points_3D = great_circle_points_3D / norm(
126
+ great_circle_points_3D, axis=1, keepdims=True
127
+ )
128
+ great_circle_points = _get_stereographic_projection(great_circle_points_3D)
129
+ ipf_boundary = np.concatenate([[[0, 0]], great_circle_points, [[0, 0]]], axis=0)
130
+ elif unit_cell.lower() in ["hexagonal", "hcp"]:
131
+ pole_1 = np.array([1, 0, 0])
132
+ pole_2 = np.array([np.sqrt(3) / 2, 0.5, 0])
133
+ great_circle_direction = pole_2 - pole_1
134
+ increments = np.linspace(0, 1, num_points)
135
+ great_circle_points_3D = (
136
+ increments[:, np.newaxis] * great_circle_direction + pole_1
137
+ )
138
+ great_circle_points_3D = great_circle_points_3D / norm(
139
+ great_circle_points_3D, axis=1, keepdims=True
140
+ )
141
+ great_circle_points = _get_stereographic_projection(great_circle_points_3D)
142
+ ipf_boundary = np.concatenate([[[0, 0]], great_circle_points, [[0, 0]]], axis=0)
143
+ return ipf_boundary
144
+
145
+
146
+ def get_symmetry_operators(unit_cell):
147
+ # source: Rollett lecture notes on symmetry; corrected from Kocks, Tome,
148
+ # and Wenk, Texture and Anisotropy
149
+ i = np.array([1, 0, 0])
150
+ j = np.array([0, 1, 0])
151
+ k = np.array([0, 0, 1])
152
+ if unit_cell.lower() in ["hexagonal", "hcp"]:
153
+ a = np.sqrt(3) / 2
154
+ p5 = 0.5
155
+ m5 = -0.5
156
+ symmetry_matrices = [0] * 12
157
+ symmetry_matrices[0] = np.eye(3)
158
+ symmetry_matrices[1] = np.array([[m5, a, 0], [-a, m5, 0], k])
159
+ symmetry_matrices[2] = np.array([[m5, -a, 0], [a, m5, 0], k])
160
+ symmetry_matrices[3] = np.array([[p5, a, 0], [-a, p5, 0], k])
161
+ symmetry_matrices[4] = np.array([-i, -j, k])
162
+ symmetry_matrices[5] = np.array([[p5, -a, 0], [a, p5, 0], k])
163
+ symmetry_matrices[6] = np.array([[m5, -a, 0], [-a, p5, 0], -k])
164
+ symmetry_matrices[7] = np.array([i, -j, -k])
165
+ symmetry_matrices[8] = np.array([[m5, a, 0], [a, p5, 0], -k])
166
+ symmetry_matrices[9] = np.array([[p5, a, 0], [a, m5, 0], -k])
167
+ symmetry_matrices[10] = np.array([-i, j, -k])
168
+ symmetry_matrices[11] = np.array([[p5, -a, 0], [-a, m5, 0], -k])
169
+
170
+ elif unit_cell.lower() in ["cubic", "fcc", "bcc"]:
171
+ symmetry_matrices = [0] * 24
172
+ symmetry_matrices[0] = np.eye(3)
173
+ symmetry_matrices[1] = np.array([k, i, j])
174
+ symmetry_matrices[2] = np.array([j, k, i])
175
+ symmetry_matrices[3] = np.array([-j, k, -i])
176
+ symmetry_matrices[4] = np.array([-j, -k, i])
177
+ symmetry_matrices[5] = np.array([j, -k, -i])
178
+ symmetry_matrices[6] = np.array([-k, i, -j])
179
+ symmetry_matrices[7] = np.array([-k, -i, j])
180
+ symmetry_matrices[8] = np.array([k, -i, -j])
181
+ symmetry_matrices[9] = np.array([-i, j, -k])
182
+ symmetry_matrices[10] = np.array([-i, -j, k])
183
+ symmetry_matrices[11] = np.array([i, -j, -k])
184
+ symmetry_matrices[12] = np.array([-k, -j, -i])
185
+ symmetry_matrices[13] = np.array([k, -j, i])
186
+ symmetry_matrices[14] = np.array([k, j, -i])
187
+ symmetry_matrices[15] = np.array([-k, j, i])
188
+ symmetry_matrices[16] = np.array([-i, -k, -j])
189
+ symmetry_matrices[17] = np.array([i, -k, j])
190
+ symmetry_matrices[18] = np.array([i, k, -j])
191
+ symmetry_matrices[19] = np.array([-i, k, j])
192
+ symmetry_matrices[20] = np.array([-j, -i, -k])
193
+ symmetry_matrices[21] = np.array([j, -i, k])
194
+ symmetry_matrices[22] = np.array([j, i, -k])
195
+ symmetry_matrices[23] = np.array([-j, i, k])
196
+
197
+ else:
198
+ raise ValueError(f"{unit_cell} is not a valid unit cell")
199
+ return np.array(symmetry_matrices)
@@ -0,0 +1,182 @@
1
+ # Copyright 2025 United States Government as represented by the Administrator of the
2
+ # National Aeronautics and Space Administration.  All Rights Reserved.
3
+ #
4
+ # The Materialite platform is licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed
9
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
11
+ # specific language governing permissions and limitations under the License.
12
+
13
+ import h5py
14
+ import numpy as np
15
+ import pandas as pd
16
+ from materialite import Material
17
+ from materialite.util import camel_to_snake
18
+
19
+
20
+ def import_dream3d(
21
+ file,
22
+ simpl_geometry_path,
23
+ field_paths=None,
24
+ region_id_path=None,
25
+ region_field_paths=None,
26
+ ):
27
+ if region_id_path is None and region_field_paths is not None:
28
+ raise ValueError(
29
+ "region_id_path must be specified if region_field_paths are provided"
30
+ )
31
+ d3d = h5py.File(file, mode="r")
32
+
33
+ simpl_geometry_data = d3d.get(simpl_geometry_path)
34
+
35
+ dimensions = list(simpl_geometry_data.get("DIMENSIONS"))
36
+ num_points = np.prod(dimensions)
37
+ origin = list(simpl_geometry_data.get("ORIGIN"))
38
+ spacing = list(simpl_geometry_data.get("SPACING"))
39
+
40
+ fields = _initialize_fields(dimensions, origin, spacing)
41
+
42
+ field_paths = [] if field_paths is None else field_paths
43
+ for field_path in field_paths:
44
+ label = camel_to_snake(field_path.split("/")[-1])
45
+ field = d3d.get(field_path)[:]
46
+ field = field.reshape((num_points, field.shape[-1]))
47
+ num_components = field.shape[1]
48
+ for ii in range(num_components):
49
+ suffix = "" if num_components == 1 else "_" + str(ii + 1)
50
+ fields[label + suffix] = field[:, ii]
51
+
52
+ regional_fields = {}
53
+ if region_id_path is not None:
54
+ region_id_label = camel_to_snake(region_id_path.split("/")[-1])
55
+ region_id_field = d3d.get(region_id_path)[:].reshape(-1)
56
+ fields[region_id_label] = region_id_field
57
+ if region_field_paths is not None:
58
+ regional_fields[region_id_label] = np.unique(region_id_field)
59
+
60
+ region_field_paths = [] if region_field_paths is None else region_field_paths
61
+ for region_field_path in region_field_paths:
62
+ region_label = camel_to_snake(region_field_path.split("/")[-1])
63
+ region_field = d3d.get(region_field_path)[:]
64
+ num_components = region_field.shape[1]
65
+ for ii in range(num_components):
66
+ suffix = "" if num_components == 1 else "_" + str(ii + 1)
67
+ regional_fields[region_label + suffix] = region_field[1:, ii]
68
+
69
+ material = Material(
70
+ dimensions=dimensions, origin=origin, spacing=spacing, fields=fields
71
+ )
72
+ if regional_fields:
73
+ material = material.create_regional_fields(
74
+ region_label=region_id_label, regional_fields=regional_fields
75
+ )
76
+ return material
77
+
78
+
79
+ def _initialize_fields(dimensions, origin, spacing):
80
+ grid = np.indices(dimensions).T.reshape(-1, 3)
81
+ return pd.DataFrame(
82
+ {
83
+ "x": grid[:, 0] * spacing[0] + origin[0],
84
+ "y": grid[:, 1] * spacing[1] + origin[1],
85
+ "z": grid[:, 2] * spacing[2] + origin[2],
86
+ }
87
+ )
88
+
89
+
90
+ def import_spparks(
91
+ file,
92
+ usecols=[1, 2, 3, 4],
93
+ names=["feature", "x", "y", "z"],
94
+ skiprows=9,
95
+ voxel_size=1,
96
+ ):
97
+ fields = pd.read_csv(
98
+ file,
99
+ sep=" ",
100
+ header=None,
101
+ skiprows=skiprows,
102
+ usecols=[1, 2, 3, 4],
103
+ names=["feature", "x", "y", "z"],
104
+ )
105
+ fields.sort_values(by=["z", "y", "x"], inplace=True)
106
+
107
+ dimensions = [fields[dim].max() - fields[dim].min() + 1 for dim in ["x", "y", "z"]]
108
+
109
+ origin = [0, 0, 0]
110
+ fields[["x", "y", "z"]] = voxel_size * fields[["x", "y", "z"]]
111
+ spacing = [voxel_size, voxel_size, voxel_size]
112
+
113
+ return Material(
114
+ dimensions=dimensions, origin=origin, spacing=spacing, fields=fields
115
+ )
116
+
117
+
118
+ def import_vgstudio(
119
+ file,
120
+ dimensions,
121
+ label="xct",
122
+ spacing=[1, 1, 1],
123
+ origin=[0, 0, 0],
124
+ dtype=np.float32,
125
+ x_range=None,
126
+ y_range=None,
127
+ z_range=None,
128
+ ):
129
+ if x_range is None:
130
+ x_range = [0, dimensions[0]]
131
+ if y_range is None:
132
+ y_range = [0, dimensions[1]]
133
+ if z_range is None:
134
+ z_range = [0, dimensions[2]]
135
+
136
+ field = np.fromfile(file, dtype)
137
+ field = field.reshape(np.array(dimensions)[::-1])
138
+ field = field[
139
+ z_range[0] : z_range[1], y_range[0] : y_range[1], x_range[0] : x_range[1]
140
+ ]
141
+ dimensions = field.shape[::-1]
142
+
143
+ return Material(
144
+ dimensions=dimensions,
145
+ spacing=spacing,
146
+ origin=origin,
147
+ fields={label: field.ravel()},
148
+ )
149
+
150
+
151
+ def import_evpfft(
152
+ file,
153
+ spacing=[1, 1, 1],
154
+ origin=[0, 0, 0],
155
+ ):
156
+
157
+ fields = pd.read_csv(
158
+ file,
159
+ header=None,
160
+ delimiter=" ",
161
+ names=[
162
+ "euler_angle_1",
163
+ "euler_angle_2",
164
+ "euler_angle_3",
165
+ "x_id",
166
+ "y_id",
167
+ "z_id",
168
+ "feature",
169
+ "phase",
170
+ ],
171
+ ).sort_values(by=["x_id", "y_id", "z_id"])
172
+
173
+ fields[["euler_angle_1", "euler_angle_2", "euler_angle_3"]] *= np.pi / 180
174
+
175
+ dimensions = fields[["x_id", "y_id", "z_id"]].max().to_numpy()
176
+
177
+ return Material(
178
+ dimensions=dimensions,
179
+ spacing=spacing,
180
+ origin=origin,
181
+ fields=fields,
182
+ )
@@ -0,0 +1,122 @@
1
+ # Copyright 2025 United States Government as represented by the Administrator of the
2
+ # National Aeronautics and Space Administration.  All Rights Reserved.
3
+ #
4
+ # The Materialite platform is licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software distributed
9
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
11
+ # specific language governing permissions and limitations under the License.
12
+
13
+ import gzip
14
+ import json
15
+
16
+ import materialite.tensor
17
+ import numpy as np
18
+ import pandas as pd
19
+ from materialite import Material
20
+ from materialite.tensor import Orientation, Tensor
21
+
22
+
23
+ class MaterialEncoder(json.JSONEncoder):
24
+ def default(self, obj):
25
+ if isinstance(obj, Tensor):
26
+ return {
27
+ "data": obj.components.tolist(),
28
+ "type": type(obj).__name__,
29
+ "dims": obj.dims_str,
30
+ }
31
+ elif isinstance(obj, Orientation):
32
+ return {"data": obj.rotation_matrix.tolist(), "type": type(obj).__name__}
33
+ elif isinstance(obj, (np.number, np.ndarray)):
34
+ return {"data": obj.tolist()}
35
+ return super().default(obj)
36
+
37
+
38
+ def decoder_object_hook(dct):
39
+ data = dct.get("data", None)
40
+ if data is None:
41
+ return dct
42
+ data_type = dct.get("type", None)
43
+ dims = dct.get("dims", None)
44
+ if data_type is None:
45
+ return data
46
+ elif dims is None:
47
+ tensor_type = getattr(materialite.tensor, data_type)
48
+ return tensor_type(data)
49
+ else:
50
+ tensor_type = getattr(materialite.tensor, data_type)
51
+ return tensor_type(data, dims)
52
+
53
+
54
+ def read_from_json(filename, decompress=False):
55
+ if decompress:
56
+ r_string = "rb"
57
+ else:
58
+ r_string = "r"
59
+ with open(filename, r_string) as f:
60
+ loaded_data = f.read()
61
+ if decompress:
62
+ loaded_data = gzip.decompress(loaded_data).decode("utf-8")
63
+ loaded_data = json.loads(loaded_data, object_hook=decoder_object_hook)
64
+ state = dict()
65
+ fields = dict()
66
+ regional_fields = dict()
67
+ for k, v in loaded_data.items():
68
+ if k == "metadata":
69
+ origin = v["origin"]
70
+ dimensions = v["dimensions"]
71
+ spacing = v["spacing"]
72
+ continue
73
+ elif k == "state":
74
+ state = v
75
+ elif k == "regional_fields":
76
+ regional_fields = v
77
+ else:
78
+ fields[k] = v
79
+ if not fields:
80
+ material = Material(dimensions=dimensions, origin=origin, spacing=spacing)
81
+ else:
82
+ fields = pd.DataFrame(fields)
83
+ material = Material(
84
+ dimensions=dimensions, origin=origin, spacing=spacing, fields=fields
85
+ )
86
+ material.state = state
87
+ for k, v in regional_fields.items():
88
+ material = material.create_regional_fields(k, pd.DataFrame(v))
89
+ return material
90
+
91
+
92
+ def write_to_json(material, filename, compress=False):
93
+ coord_labels = ["x", "y", "z", "x_id", "y_id", "z_id"]
94
+ labels = list(material.fields.drop(coord_labels, axis=1))
95
+ material_dict = {
96
+ "metadata": {
97
+ "origin": material.origin.tolist(),
98
+ "dimensions": material.dimensions.tolist(),
99
+ "spacing": material.spacing.tolist(),
100
+ }
101
+ }
102
+ if material.state:
103
+ material_dict["state"] = material.state
104
+ if material._regional_fields:
105
+ material_dict["regional_fields"] = dict()
106
+ for k in material._regional_fields.keys():
107
+ regional_labels = list(material.extract_regional_field(k))
108
+ material_dict["regional_fields"][k] = dict()
109
+ for l in regional_labels:
110
+ material_dict["regional_fields"][k][l] = (
111
+ material.extract_regional_field(k, l)
112
+ )
113
+ for l in labels:
114
+ material_dict[l] = material.extract(l)
115
+ json_material = json.dumps(material_dict, cls=MaterialEncoder)
116
+ if compress:
117
+ json_material = gzip.compress(json_material.encode("utf-8"))
118
+ w_string = "wb"
119
+ else:
120
+ w_string = "w"
121
+ with open(filename, w_string) as f:
122
+ f.write(json_material)