nomad-schema-plugin-simulation-workflow 1.0.1__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,494 @@
1
+ #
2
+ # Copyright The NOMAD Authors.
3
+ #
4
+ # This file is part of NOMAD. See https://nomad-lab.eu for further info.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ import numpy as np
19
+
20
+ from nomad.datamodel.data import ArchiveSection
21
+ from nomad.metainfo import SubSection, Section, Quantity
22
+ from nomad.datamodel.metainfo.workflow import Link
23
+ from .general import (
24
+ SimulationWorkflowMethod,
25
+ SimulationWorkflowResults,
26
+ ParallelSimulation,
27
+ WORKFLOW_METHOD_NAME,
28
+ WORKFLOW_RESULTS_NAME,
29
+ )
30
+ from .thermodynamics import ThermodynamicsResults
31
+
32
+
33
+ class StrainDiagrams(ArchiveSection):
34
+ """
35
+ Section containing the information regarding the elastic strains.
36
+ """
37
+
38
+ m_def = Section(validate=False)
39
+
40
+ type = Quantity(
41
+ type=str,
42
+ shape=[],
43
+ description="""
44
+ Kind of strain diagram. Possible values are: energy; cross-validation (cross-
45
+ validation error); d2E (second derivative of the energy wrt the strain)
46
+ """,
47
+ )
48
+
49
+ n_eta = Quantity(
50
+ type=np.int32,
51
+ shape=[],
52
+ description="""
53
+ Number of strain values used in the strain diagram
54
+ """,
55
+ )
56
+
57
+ n_deformations = Quantity(
58
+ type=np.int32,
59
+ shape=[],
60
+ description="""
61
+ Number of deformations.
62
+ """,
63
+ )
64
+
65
+ value = Quantity(
66
+ type=np.float64,
67
+ shape=['n_deformations', 'n_eta'],
68
+ description="""
69
+ Values of the energy(units:J)/d2E(units:Pa)/cross-validation (depending on the
70
+ value of strain_diagram_type)
71
+ """,
72
+ )
73
+
74
+ eta = Quantity(
75
+ type=np.float64,
76
+ shape=['n_deformations', 'n_eta'],
77
+ description="""
78
+ eta values used the strain diagrams
79
+ """,
80
+ )
81
+
82
+ stress_voigt_component = Quantity(
83
+ type=np.int32,
84
+ shape=[],
85
+ description="""
86
+ Voigt component corresponding to the strain diagram
87
+ """,
88
+ )
89
+
90
+ polynomial_fit_order = Quantity(
91
+ type=np.int32,
92
+ shape=[],
93
+ description="""
94
+ Order of the polynomial fit
95
+ """,
96
+ )
97
+
98
+
99
+ class ElasticMethod(SimulationWorkflowMethod):
100
+ energy_stress_calculator = Quantity(
101
+ type=str,
102
+ shape=[],
103
+ description="""
104
+ Name of program used to calculate energy or stress.
105
+ """,
106
+ )
107
+
108
+ calculation_method = Quantity(
109
+ type=str,
110
+ shape=[],
111
+ description="""
112
+ Method used to calculate elastic constants, can either be energy or stress.
113
+ """,
114
+ )
115
+
116
+ elastic_constants_order = Quantity(
117
+ type=int,
118
+ shape=[],
119
+ description="""
120
+ Order of the calculated elastic constants.
121
+ """,
122
+ )
123
+
124
+ fitting_error_maximum = Quantity(
125
+ type=np.float64,
126
+ shape=[],
127
+ description="""
128
+ Maximum error in polynomial fit.
129
+ """,
130
+ )
131
+
132
+ strain_maximum = Quantity(
133
+ type=np.float64,
134
+ shape=[],
135
+ description="""
136
+ Maximum strain applied to crystal.
137
+ """,
138
+ )
139
+
140
+
141
+ class ElasticResults(ThermodynamicsResults):
142
+ n_deformations = Quantity(
143
+ type=np.int32,
144
+ shape=[],
145
+ description="""
146
+ Number of deformed structures used to calculate the elastic constants. This is
147
+ determined by the symmetry of the crystal.
148
+ """,
149
+ )
150
+
151
+ deformation_types = Quantity(
152
+ type=np.str_,
153
+ shape=['n_deformations', 6],
154
+ description="""
155
+ deformation types
156
+ """,
157
+ )
158
+
159
+ n_strains = Quantity(
160
+ type=np.int32,
161
+ shape=[],
162
+ description="""
163
+ number of equally spaced strains applied to each deformed structure, which are
164
+ generated between the maximum negative strain and the maximum positive one.
165
+ """,
166
+ )
167
+
168
+ is_mechanically_stable = Quantity(
169
+ type=bool,
170
+ shape=[],
171
+ description="""
172
+ Indicates if structure is mechanically stable from the calculated values of the
173
+ elastic constants.
174
+ """,
175
+ )
176
+
177
+ elastic_constants_notation_matrix_second_order = Quantity(
178
+ type=np.str_,
179
+ shape=[6, 6],
180
+ description="""
181
+ Symmetry of the second-order elastic constant matrix in Voigt notation
182
+ """,
183
+ )
184
+
185
+ elastic_constants_matrix_second_order = Quantity(
186
+ type=np.float64,
187
+ shape=[6, 6],
188
+ unit='pascal',
189
+ description="""
190
+ 2nd order elastic constant (stiffness) matrix in pascals
191
+ """,
192
+ )
193
+
194
+ elastic_constants_matrix_third_order = Quantity(
195
+ type=np.float64,
196
+ shape=[6, 6, 6],
197
+ unit='pascal',
198
+ description="""
199
+ 3rd order elastic constant (stiffness) matrix in pascals
200
+ """,
201
+ )
202
+
203
+ compliance_matrix_second_order = Quantity(
204
+ type=np.float64,
205
+ shape=[6, 6],
206
+ unit='1 / pascal',
207
+ description="""
208
+ Elastic compliance matrix in 1/GPa
209
+ """,
210
+ )
211
+
212
+ elastic_constants_gradient_matrix_second_order = Quantity(
213
+ type=np.float64,
214
+ shape=[18, 18],
215
+ unit='newton',
216
+ description="""
217
+ gradient of the 2nd order elastic constant (stiffness) matrix in newton
218
+ """,
219
+ )
220
+
221
+ bulk_modulus_voigt = Quantity(
222
+ type=np.float64,
223
+ shape=[],
224
+ unit='pascal',
225
+ description="""
226
+ Voigt bulk modulus
227
+ """,
228
+ )
229
+
230
+ shear_modulus_voigt = Quantity(
231
+ type=np.float64,
232
+ shape=[],
233
+ unit='pascal',
234
+ description="""
235
+ Voigt shear modulus
236
+ """,
237
+ )
238
+
239
+ bulk_modulus_reuss = Quantity(
240
+ type=np.float64,
241
+ shape=[],
242
+ unit='pascal',
243
+ description="""
244
+ Reuss bulk modulus
245
+ """,
246
+ )
247
+
248
+ shear_modulus_reuss = Quantity(
249
+ type=np.float64,
250
+ shape=[],
251
+ unit='pascal',
252
+ description="""
253
+ Reuss shear modulus
254
+ """,
255
+ )
256
+
257
+ bulk_modulus_hill = Quantity(
258
+ type=np.float64,
259
+ shape=[],
260
+ unit='pascal',
261
+ description="""
262
+ Hill bulk modulus
263
+ """,
264
+ )
265
+
266
+ shear_modulus_hill = Quantity(
267
+ type=np.float64,
268
+ shape=[],
269
+ unit='pascal',
270
+ description="""
271
+ Hill shear modulus
272
+ """,
273
+ )
274
+
275
+ young_modulus_voigt = Quantity(
276
+ type=np.float64,
277
+ shape=[],
278
+ unit='pascal',
279
+ description="""
280
+ Voigt Young modulus
281
+ """,
282
+ )
283
+
284
+ poisson_ratio_voigt = Quantity(
285
+ type=np.float64,
286
+ shape=[],
287
+ description="""
288
+ Voigt Poisson ratio
289
+ """,
290
+ )
291
+
292
+ young_modulus_reuss = Quantity(
293
+ type=np.float64,
294
+ shape=[],
295
+ unit='pascal',
296
+ description="""
297
+ Reuss Young modulus
298
+ """,
299
+ )
300
+
301
+ poisson_ratio_reuss = Quantity(
302
+ type=np.float64,
303
+ shape=[],
304
+ description="""
305
+ Reuss Poisson ratio
306
+ """,
307
+ )
308
+
309
+ young_modulus_hill = Quantity(
310
+ type=np.float64,
311
+ shape=[],
312
+ unit='pascal',
313
+ description="""
314
+ Hill Young modulus
315
+ """,
316
+ )
317
+
318
+ poisson_ratio_hill = Quantity(
319
+ type=np.float64,
320
+ shape=[],
321
+ description="""
322
+ Hill Poisson ratio
323
+ """,
324
+ )
325
+
326
+ elastic_anisotropy = Quantity(
327
+ type=np.float64,
328
+ shape=[],
329
+ description="""
330
+ Elastic anisotropy
331
+ """,
332
+ )
333
+
334
+ pugh_ratio_hill = Quantity(
335
+ type=np.float64,
336
+ shape=[],
337
+ description="""
338
+ Pugh ratio defined as the ratio between the shear modulus and bulk modulus
339
+ """,
340
+ )
341
+
342
+ debye_temperature = Quantity(
343
+ type=np.float64,
344
+ shape=[],
345
+ unit='kelvin',
346
+ description="""
347
+ Debye temperature
348
+ """,
349
+ )
350
+
351
+ speed_sound_transverse = Quantity(
352
+ type=np.float64,
353
+ shape=[],
354
+ unit='meter / second',
355
+ description="""
356
+ Speed of sound along the transverse direction
357
+ """,
358
+ )
359
+
360
+ speed_sound_longitudinal = Quantity(
361
+ type=np.float64,
362
+ shape=[],
363
+ unit='meter / second',
364
+ description="""
365
+ Speed of sound along the longitudinal direction
366
+ """,
367
+ )
368
+
369
+ speed_sound_average = Quantity(
370
+ type=np.float64,
371
+ shape=[],
372
+ unit='meter / second',
373
+ description="""
374
+ Average speed of sound
375
+ """,
376
+ )
377
+
378
+ eigenvalues_elastic = Quantity(
379
+ type=np.float64,
380
+ shape=[6],
381
+ unit='pascal',
382
+ description="""
383
+ Eigenvalues of the stiffness matrix
384
+ """,
385
+ )
386
+
387
+ strain_diagrams = SubSection(sub_section=StrainDiagrams.m_def, repeats=True)
388
+
389
+
390
+ class Elastic(ParallelSimulation):
391
+ method = SubSection(sub_section=ElasticMethod)
392
+
393
+ results = SubSection(sub_section=ElasticResults)
394
+
395
+ def _resolve_mechanical_stability(self):
396
+ spacegroup, c = None, None
397
+ try:
398
+ spacegroup = self._systems[-1].symmetry[-1].space_group_number
399
+ c = self.results.elastic_constants_matrix_second_order
400
+ except Exception:
401
+ return False
402
+
403
+ if c is None or spacegroup is None:
404
+ return False
405
+
406
+ # see Phys. Rev B 90, 224104 (2014)
407
+ res = False
408
+ if spacegroup <= 2: # Triclinic
409
+ res = np.count_nonzero(c < 0)
410
+ elif spacegroup <= 15: # Monoclinic
411
+ res = np.count_nonzero(c < 0)
412
+ elif spacegroup <= 74: # Orthorhombic
413
+ res = (
414
+ c[0][0] > 0
415
+ and c[0][0] * c[1][1] > c[0][1] ** 2
416
+ and c[0][0] * c[1][1] * c[2][2]
417
+ + 2 * c[0][1] * c[0][2] * c[1][2]
418
+ - c[0][0] * c[1][2] ** 2
419
+ - c[1][1] * c[0][2] ** 2
420
+ - c[2][2] * c[0][1] ** 2
421
+ > 0
422
+ and c[3][3] > 0
423
+ and c[4][4] > 0
424
+ and c[5][5] > 0
425
+ )
426
+ elif spacegroup <= 88: # Tetragonal II
427
+ res = c[0][0] > abs(c[0][1]) and 2 * c[0][2] ** 2 < c[2][2] * (
428
+ c[0][0] + c[0][1]
429
+ )
430
+ elif spacegroup <= 142: # Tetragonal I
431
+ res = (
432
+ c[0][0] > abs(c[0][1])
433
+ and 2 * c[0][2] ** 2 < c[2][2] * (c[0][0] + c[0][1])
434
+ and c[3][3] > 0
435
+ and c[5][5] > 0
436
+ )
437
+ elif spacegroup <= 148: # rhombohedral II
438
+ res = (
439
+ c[0][0] > abs(c[0][1])
440
+ and c[3][3] > 0
441
+ and c[0][2] ** 2 < (0.5 * c[2][2] * (c[0][0] + c[0][1]))
442
+ and c[0][3] ** 2 + c[0][4] ** 2 < 0.5 * c[3][3] * (c[0][0] - c[0][1])
443
+ )
444
+ elif spacegroup <= 167: # rhombohedral I
445
+ res = (
446
+ c[0][0] > abs(c[0][1])
447
+ and c[3][3] > 0
448
+ and c[0][2] ** 2 < 0.5 * c[2][2] * (c[0][0] + c[0][1])
449
+ and c[0][3] ** 2 < 0.5 * c[3][3] * (c[0][0] - c[0][1])
450
+ )
451
+ elif spacegroup <= 194: # hexagonal I
452
+ res = (
453
+ c[0][0] > abs(c[0][1])
454
+ and 2 * c[0][2] ** 2 < c[2][2] * (c[0][0] + c[0][1])
455
+ and c[3][3] > 0
456
+ and c[5][5] > 0
457
+ )
458
+ else: # cubic
459
+ res = c[0][0] - c[0][1] > 0 and c[0][0] + 2 * c[0][1] > 0 and c[3][3] > 0
460
+
461
+ return res
462
+
463
+ def _get_maximum_fit_error(self):
464
+ max_error = 0.0
465
+ if len(self._calculations) == 0:
466
+ return max_error
467
+
468
+ for diagram in self.results.strain_diagrams:
469
+ if diagram.type == 'cross-validation':
470
+ error = np.amax(diagram.value)
471
+ max_error = error if error > max_error else max_error
472
+
473
+ return max_error
474
+
475
+ def normalize(self, archive, logger):
476
+ super().normalize(archive, logger)
477
+
478
+ if not self.method:
479
+ self.method = ElasticMethod()
480
+ self.inputs.append(Link(name=WORKFLOW_METHOD_NAME, section=self.method))
481
+
482
+ if not self.results:
483
+ self.results = ElasticResults()
484
+ self.outputs.append(Link(name=WORKFLOW_RESULTS_NAME, section=self.results))
485
+
486
+ if self.results.is_mechanically_stable is None:
487
+ self.results.is_mechanically_stable = bool(
488
+ self._resolve_mechanical_stability()
489
+ )
490
+
491
+ if self.method.fitting_error_maximum is None:
492
+ self.method.fitting_error_maximum = self._get_maximum_fit_error()
493
+
494
+ SimulationWorkflowResults.normalize(self.results, archive, logger)
@@ -0,0 +1,213 @@
1
+ #
2
+ # Copyright The NOMAD Authors.
3
+ #
4
+ # This file is part of NOMAD. See https://nomad-lab.eu for further info.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ import numpy as np
19
+ from ase.eos import EquationOfState as aseEOS
20
+
21
+ from nomad.atomutils import get_volume
22
+ from nomad.datamodel.data import ArchiveSection
23
+ from nomad.units import ureg
24
+ from nomad.metainfo import SubSection, Section, Quantity
25
+ from nomad.datamodel.metainfo.workflow import Link
26
+ from .general import (
27
+ SimulationWorkflowMethod,
28
+ SimulationWorkflowResults,
29
+ ParallelSimulation,
30
+ WORKFLOW_METHOD_NAME,
31
+ WORKFLOW_RESULTS_NAME,
32
+ )
33
+
34
+
35
+ class EquationOfStateMethod(SimulationWorkflowMethod):
36
+ energy_calculator = Quantity(
37
+ type=str,
38
+ shape=[],
39
+ description="""
40
+ Name of program used to calculate energy.
41
+ """,
42
+ )
43
+
44
+
45
+ class EOSFit(ArchiveSection):
46
+ """
47
+ Section containing results of an equation of state fit.
48
+ """
49
+
50
+ m_def = Section(validate=False)
51
+
52
+ function_name = Quantity(
53
+ type=str,
54
+ shape=[],
55
+ description="""
56
+ Specifies the function used to perform the fitting of the volume-energy data. Value
57
+ can be one of birch_euler, birch_lagrange, birch_murnaghan, mie_gruneisen,
58
+ murnaghan, pack_evans_james, poirier_tarantola, tait, vinet.
59
+ """,
60
+ )
61
+
62
+ fitted_energies = Quantity(
63
+ type=np.float64,
64
+ shape=['n_points'],
65
+ unit='joule',
66
+ description="""
67
+ Array of the fitted energies corresponding to each volume.
68
+ """,
69
+ )
70
+
71
+ bulk_modulus = Quantity(
72
+ type=np.float64,
73
+ shape=[],
74
+ unit='pascal',
75
+ description="""
76
+ Calculated value of the bulk modulus by fitting the volume-energy data.
77
+ """,
78
+ )
79
+
80
+ bulk_modulus_derivative = Quantity(
81
+ type=np.float64,
82
+ shape=[],
83
+ description="""
84
+ Calculated value of the pressure derivative of the bulk modulus.
85
+ """,
86
+ )
87
+
88
+ equilibrium_volume = Quantity(
89
+ type=np.float64,
90
+ shape=[],
91
+ unit='m ** 3',
92
+ description="""
93
+ Calculated value of the equilibrium volume by fitting the volume-energy data.
94
+ """,
95
+ )
96
+
97
+ equilibrium_energy = Quantity(
98
+ type=np.float64,
99
+ shape=[],
100
+ unit='joule',
101
+ description="""
102
+ Calculated value of the equilibrium energy by fitting the volume-energy data.
103
+ """,
104
+ )
105
+
106
+ rms_error = Quantity(
107
+ type=np.float64,
108
+ shape=[],
109
+ description="""
110
+ Root-mean squared value of the error in the fitting.
111
+ """,
112
+ )
113
+
114
+
115
+ class EquationOfStateResults(SimulationWorkflowResults):
116
+ n_points = Quantity(
117
+ type=np.int32,
118
+ shape=[],
119
+ description="""
120
+ Number of volume-energy pairs in data.
121
+ """,
122
+ )
123
+
124
+ volumes = Quantity(
125
+ type=np.float64,
126
+ shape=['n_points'],
127
+ unit='m ** 3',
128
+ description="""
129
+ Array of volumes per atom for which the energies are evaluated.
130
+ """,
131
+ )
132
+
133
+ energies = Quantity(
134
+ type=np.float64,
135
+ shape=['n_points'],
136
+ unit='joule',
137
+ description="""
138
+ Array of energies corresponding to each volume.
139
+ """,
140
+ )
141
+
142
+ eos_fit = SubSection(sub_section=EOSFit.m_def, repeats=True)
143
+
144
+
145
+ class EquationOfState(ParallelSimulation):
146
+ method = SubSection(sub_section=EquationOfStateMethod)
147
+
148
+ results = SubSection(sub_section=EquationOfStateResults)
149
+
150
+ def normalize(self, archive, logger):
151
+ super().normalize(archive, logger)
152
+
153
+ if not self.method:
154
+ self.method = EquationOfStateMethod()
155
+ self.inputs.append(Link(name=WORKFLOW_METHOD_NAME, section=self.method))
156
+
157
+ if not self.results:
158
+ self.results = EquationOfStateResults()
159
+ self.outputs.append(Link(name=WORKFLOW_RESULTS_NAME, section=self.results))
160
+
161
+ if not self._calculations:
162
+ return
163
+
164
+ if self.results.energies is None:
165
+ try:
166
+ self.results.energies = [
167
+ calc.energy.total.value.magnitude for calc in self._calculations
168
+ ]
169
+ except Exception:
170
+ pass
171
+
172
+ if self.results.volumes is None:
173
+ try:
174
+ volumes = []
175
+ unit = 1
176
+ for system in self._systems:
177
+ if system.atoms.lattice_vectors is not None:
178
+ cell = system.atoms.lattice_vectors.magnitude
179
+ unit = system.atoms.lattice_vectors.units
180
+ volumes.append(get_volume(cell))
181
+ self.results.volumes = np.array(volumes) * unit**3
182
+ except Exception:
183
+ pass
184
+
185
+ if not self.results.eos_fit:
186
+ function_name_map = {
187
+ 'birch_murnaghan': 'birchmurnaghan',
188
+ 'pourier_tarantola': 'pouriertarantola',
189
+ 'vinet': 'vinet',
190
+ 'murnaghan': 'murnaghan',
191
+ 'birch_euler': 'birch',
192
+ }
193
+ if self.results.volumes is not None and self.results.energies is not None:
194
+ # convert to ase units in order for function optimization to work
195
+ volumes = self.results.volumes.to('angstrom ** 3').magnitude
196
+ energies = self.results.energies.to('eV').magnitude
197
+ for function_name, ase_name in function_name_map.items():
198
+ try:
199
+ eos = aseEOS(volumes, energies, ase_name)
200
+ eos.fit()
201
+ fitted_energies = eos.func(volumes, *eos.eos_parameters)
202
+ rms_error = np.sqrt(np.mean((fitted_energies - energies) ** 2))
203
+ eos_fit = EOSFit(
204
+ function_name=function_name,
205
+ fitted_energies=fitted_energies * ureg.eV,
206
+ bulk_modulus=eos.B * ureg.eV / ureg.angstrom**3,
207
+ equilibrium_volume=eos.v0 * ureg.angstrom**3,
208
+ equilibrium_energy=eos.e0 * ureg.eV,
209
+ rms_error=rms_error,
210
+ )
211
+ self.results.eos_fit.append(eos_fit)
212
+ except Exception:
213
+ self.logger.warning('EOS fit not succesful.')