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,349 @@
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 import Atoms
20
+
21
+ from nomad.metainfo import SubSection, Quantity, MEnum
22
+ from nomad.datamodel.metainfo.workflow import Link
23
+ from runschema.calculation import EnergyEntry
24
+
25
+ from .general import (
26
+ SimulationWorkflowMethod,
27
+ SimulationWorkflowResults,
28
+ SerialSimulation,
29
+ WORKFLOW_METHOD_NAME,
30
+ WORKFLOW_RESULTS_NAME,
31
+ resolve_difference,
32
+ )
33
+
34
+
35
+ class GeometryOptimizationMethod(SimulationWorkflowMethod):
36
+ type = Quantity(
37
+ type=MEnum('static', 'atomic', 'cell_shape', 'cell_volume'),
38
+ shape=[],
39
+ description="""
40
+ The type of geometry optimization, which denotes what is being optimized.
41
+
42
+ Allowed values are:
43
+
44
+ | Type | Description |
45
+
46
+ | ---------------------- | ----------------------------------------- |
47
+
48
+ | `"static"` | no optimization |
49
+
50
+ | `"atomic"` | the atomic coordinates alone are updated |
51
+
52
+ | `"cell_volume"` | `"atomic"` + cell lattice paramters are updated isotropically |
53
+
54
+ | `"cell_shape"` | `"cell_volume"` but without the isotropic constraint: all cell parameters are updated |
55
+
56
+ """,
57
+ )
58
+
59
+ method = Quantity(
60
+ type=str,
61
+ shape=[],
62
+ description="""
63
+ The method used for geometry optimization. Some known possible values are:
64
+ `"steepest_descent"`, `"conjugant_gradient"`, `"low_memory_broyden_fletcher_goldfarb_shanno"`.
65
+ """,
66
+ )
67
+
68
+ convergence_tolerance_energy_difference = Quantity(
69
+ type=np.float64,
70
+ shape=[],
71
+ unit='joule',
72
+ description="""
73
+ The input energy difference tolerance criterion.
74
+ """,
75
+ )
76
+
77
+ convergence_tolerance_force_maximum = Quantity(
78
+ type=np.float64,
79
+ shape=[],
80
+ unit='newton',
81
+ description="""
82
+ The input maximum net force tolerance criterion.
83
+ """,
84
+ )
85
+
86
+ convergence_tolerance_stress_maximum = Quantity(
87
+ type=np.float64,
88
+ shape=[],
89
+ unit='pascal',
90
+ description="""
91
+ The input maximum stress tolerance criterion.
92
+ """,
93
+ )
94
+
95
+ convergence_tolerance_displacement_maximum = Quantity(
96
+ type=np.float64,
97
+ shape=[],
98
+ unit='meter',
99
+ description="""
100
+ The input maximum displacement tolerance criterion.
101
+ """,
102
+ )
103
+
104
+ optimization_steps_maximum = Quantity(
105
+ type=int,
106
+ shape=[],
107
+ description="""
108
+ Maximum number of optimization steps.
109
+ """,
110
+ )
111
+
112
+ save_frequency = Quantity(
113
+ type=int,
114
+ shape=[],
115
+ description="""
116
+ The number of optimization steps between saving the calculation.
117
+ """,
118
+ )
119
+
120
+
121
+ class GeometryOptimizationResults(SimulationWorkflowResults):
122
+ optimization_steps = Quantity(
123
+ type=int,
124
+ shape=[],
125
+ description="""
126
+ Number of saved optimization steps.
127
+ """,
128
+ )
129
+
130
+ energies = Quantity(
131
+ type=np.float64,
132
+ unit='joule',
133
+ shape=['optimization_steps'],
134
+ description="""
135
+ List of energy_total values gathered from the single configuration
136
+ calculations that are a part of the optimization trajectory.
137
+ """,
138
+ )
139
+
140
+ steps = Quantity(
141
+ type=np.int32,
142
+ shape=['optimization_steps'],
143
+ description="""
144
+ The step index corresponding to each saved configuration.
145
+ """,
146
+ )
147
+
148
+ final_energy_difference = Quantity(
149
+ type=np.float64,
150
+ shape=[],
151
+ unit='joule',
152
+ description="""
153
+ The difference in the energy_total between the last two steps during
154
+ optimization.
155
+ """,
156
+ )
157
+
158
+ final_force_maximum = Quantity(
159
+ type=np.float64,
160
+ shape=[],
161
+ unit='newton',
162
+ description="""
163
+ The maximum net force in the last optimization step.
164
+ """,
165
+ )
166
+
167
+ final_displacement_maximum = Quantity(
168
+ type=np.float64,
169
+ shape=[],
170
+ unit='meter',
171
+ description="""
172
+ The maximum displacement in the last optimization step with respect to previous.
173
+ """,
174
+ )
175
+
176
+ is_converged_geometry = Quantity(
177
+ type=bool,
178
+ shape=[],
179
+ description="""
180
+ Indicates if the geometry convergence criteria were fulfilled.
181
+ """,
182
+ )
183
+
184
+
185
+ class GeometryOptimization(SerialSimulation):
186
+ method = SubSection(sub_section=GeometryOptimizationMethod)
187
+
188
+ results = SubSection(sub_section=GeometryOptimizationResults)
189
+
190
+ def _get_geometry_optimization_type(self):
191
+ if not self._systems:
192
+ return
193
+
194
+ def compare_cell(cell1, cell2):
195
+ if (cell1 == cell2).all():
196
+ return None
197
+ else:
198
+ cell1_normed = cell1 / np.linalg.norm(cell1)
199
+ cell2_normed = cell2 / np.linalg.norm(cell2)
200
+ if (cell1_normed == cell2_normed).all():
201
+ return 'cell_volume'
202
+ else:
203
+ return 'cell_shape'
204
+
205
+ if len(self._systems) < 2:
206
+ return 'static'
207
+
208
+ else:
209
+ if self._systems[0].atoms is None or self._systems[-1].atoms is None:
210
+ return 'static'
211
+
212
+ cell_init = self._systems[0].atoms.lattice_vectors
213
+ cell_final = self._systems[-1].atoms.lattice_vectors
214
+ if cell_init is None or cell_final is None:
215
+ return 'static'
216
+
217
+ cell_relaxation = compare_cell(cell_init.magnitude, cell_final.magnitude)
218
+
219
+ if cell_relaxation is not None:
220
+ return cell_relaxation
221
+
222
+ atom_pos_init = self._systems[0].atoms.positions
223
+ atom_pos_final = self._systems[-1].atoms.positions
224
+ if atom_pos_init is None or atom_pos_final is None:
225
+ return 'static'
226
+
227
+ if (atom_pos_init.magnitude == atom_pos_final.magnitude).all():
228
+ return 'static'
229
+
230
+ return 'atomic'
231
+
232
+ def normalize(self, archive, logger):
233
+ super().normalize(archive, logger)
234
+
235
+ if not self.method:
236
+ self.method = GeometryOptimizationMethod()
237
+ self.inputs.append(Link(name=WORKFLOW_METHOD_NAME, section=self.method))
238
+
239
+ if not self.results:
240
+ self.results = GeometryOptimizationResults()
241
+ self.outputs.append(Link(name=WORKFLOW_RESULTS_NAME, section=self.results))
242
+
243
+ if not self.method.type and self._systems:
244
+ self.method.type = self._get_geometry_optimization_type()
245
+
246
+ if not self.results.optimization_steps:
247
+ self.results.optimization_steps = len(self._calculations)
248
+
249
+ energies = []
250
+ invalid = False
251
+ for calc in self._calculations:
252
+ try:
253
+ energy = calc.energy.total.value
254
+ except (IndexError, AttributeError):
255
+ invalid = True
256
+ break
257
+ if energy is None:
258
+ invalid = True
259
+ break
260
+ energies.append(energy.magnitude)
261
+ if invalid:
262
+ logger.warning(
263
+ 'Energy not reported for an calculation that is part of a geometry optimization'
264
+ )
265
+ if energies:
266
+ self.results.energies = energies * EnergyEntry.value.unit
267
+
268
+ energy_difference = resolve_difference(energies)
269
+ if not self.results.final_energy_difference and energy_difference is not None:
270
+ self.results.final_energy_difference = (
271
+ energy_difference * EnergyEntry.value.unit
272
+ )
273
+
274
+ if not self.results.final_force_maximum:
275
+ if len(self._calculations) > 0:
276
+ if (
277
+ self._calculations[-1].forces is not None
278
+ and self._calculations[-1].forces.total is not None
279
+ ):
280
+ forces = self._calculations[-1].forces.total.value
281
+ if forces is not None:
282
+ max_force = np.max(np.linalg.norm(forces.magnitude, axis=1))
283
+ self.results.final_force_maximum = max_force * forces.units
284
+
285
+ if not self.results.final_displacement_maximum and self._systems:
286
+
287
+ def get_atoms_positions(index):
288
+ system = self._systems[index]
289
+ if (
290
+ not system.atoms
291
+ or system.atoms.positions is None
292
+ or system.atoms.lattice_vectors is None
293
+ ):
294
+ return
295
+ try:
296
+ atoms = Atoms(
297
+ positions=system.atoms.positions.magnitude,
298
+ cell=system.atoms.lattice_vectors.magnitude,
299
+ pbc=system.atoms.periodic,
300
+ )
301
+ atoms.wrap()
302
+ return atoms.get_positions()
303
+ except Exception:
304
+ return
305
+
306
+ n_systems = len(self._systems)
307
+ a_pos = get_atoms_positions(n_systems - 1)
308
+ if a_pos is not None:
309
+ for i in range(n_systems - 2, -1, -1):
310
+ b_pos = get_atoms_positions(i)
311
+ if b_pos is None:
312
+ continue
313
+ displacement_maximum = np.max(np.abs(a_pos - b_pos))
314
+ if displacement_maximum > 0:
315
+ self.results.final_displacement_maximum = displacement_maximum
316
+ break
317
+
318
+ if not self.results.is_converged_geometry:
319
+ # we can have several criteria for convergence: energy, force, displacement
320
+ criteria = []
321
+ try:
322
+ criteria.append(
323
+ self.results.final_energy_difference
324
+ <= self.method.convergence_tolerance_energy_difference
325
+ )
326
+ except Exception:
327
+ pass
328
+
329
+ try:
330
+ criteria.append(
331
+ self.results.final_force_maximum
332
+ <= self.method.convergence_tolerance_force_maximum
333
+ )
334
+ except Exception:
335
+ pass
336
+
337
+ try:
338
+ criteria.append(
339
+ self.results.final_displacement_maximum
340
+ <= self.method.convergence_tolerance_displacement_maximum
341
+ )
342
+ except Exception:
343
+ pass
344
+
345
+ # converged when either criterion is met
346
+ if criteria:
347
+ self.results.is_converged_geometry = True in criteria
348
+
349
+ SimulationWorkflowResults.normalize(self.results, archive, logger)
@@ -0,0 +1,69 @@
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
+ from nomad.metainfo import SubSection, Quantity, Reference
19
+ from runschema.method import GW as GWMethodology
20
+ from .general import (
21
+ SimulationWorkflowResults,
22
+ ElectronicStructureOutputs,
23
+ DFTMethod,
24
+ BeyondDFT,
25
+ )
26
+
27
+
28
+ class DFTPlusGWResults(SimulationWorkflowResults):
29
+ """
30
+ Groups DFT and GW outputs: band gaps, DOS, band structures. The ResultsNormalizer
31
+ takes care of adding a label 'DFT' or 'GW' in the method `get_gw_workflow_properties`.
32
+ """
33
+
34
+ dft_outputs = SubSection(
35
+ sub_section=ElectronicStructureOutputs.m_def, repeats=False
36
+ )
37
+
38
+ gw_outputs = SubSection(sub_section=ElectronicStructureOutputs.m_def, repeats=False)
39
+
40
+
41
+ class DFTPlusGWMethod(DFTMethod):
42
+ """
43
+ Specifies both DFT and GW input methodologies: starting XC functional, electrons
44
+ representation (basis set), GW method reference.
45
+ """
46
+
47
+ gw_method_ref = Quantity(
48
+ type=Reference(GWMethodology),
49
+ description="""
50
+ Reference to the GW methodology.
51
+ """,
52
+ )
53
+
54
+
55
+ class DFTPlusGW(BeyondDFT):
56
+ """
57
+ The GW workflow is generated in an extra EntryArchive IF both the DFT SinglePoint
58
+ and the GW SinglePoint EntryArchives are present in the upload.
59
+ """
60
+
61
+ method = SubSection(sub_section=DFTPlusGWMethod)
62
+
63
+ results = SubSection(sub_section=DFTPlusGWResults)
64
+
65
+ def normalize(self, archive, logger):
66
+ if not self.results: # creates Results section if not present
67
+ self.results = DFTPlusGWResults()
68
+
69
+ super().normalize(archive, logger)
@@ -0,0 +1,80 @@
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
+ from nomad.metainfo import SubSection, Quantity, Reference
19
+ from runschema.method import Method, DMFT as DMFTMethodology
20
+ from .general import (
21
+ SimulationWorkflowResults,
22
+ ElectronicStructureOutputs,
23
+ SimulationWorkflowMethod,
24
+ BeyondDFT,
25
+ )
26
+
27
+
28
+ class DMFTPlusMaxEntResults(SimulationWorkflowResults):
29
+ """
30
+ Groups DMFT and MaxEnt outputs: greens functions (DMFT, MaxEnt), band gaps (MaxEnt),
31
+ DOS (MaxEnt), band structures (MaxEnt). The ResultsNormalizer takes care of adding a
32
+ label 'DMFT' or 'MaxEnt' in the method `get_maxent_workflow_properties`.
33
+ """
34
+
35
+ dmft_outputs = SubSection(
36
+ sub_section=ElectronicStructureOutputs.m_def, repeats=False
37
+ )
38
+
39
+ maxent_outputs = SubSection(
40
+ sub_section=ElectronicStructureOutputs.m_def, repeats=False
41
+ )
42
+
43
+
44
+ class DMFTPlusMaxEntMethod(SimulationWorkflowMethod):
45
+ """
46
+ Specifies both DMFT and MaxEnt input methodologies: DMFT method references, MaxEnt method
47
+ reference.
48
+ """
49
+
50
+ dmft_method_ref = Quantity(
51
+ type=Reference(DMFTMethodology),
52
+ description="""
53
+ DMFT methodology reference.
54
+ """,
55
+ )
56
+
57
+ # TODO define MaxEnt metainfo in Method
58
+ maxent_method_ref = Quantity(
59
+ type=Reference(Method),
60
+ description="""
61
+ MaxEnt methodology reference.
62
+ """,
63
+ )
64
+
65
+
66
+ class DMFTPlusMaxEnt(BeyondDFT):
67
+ """
68
+ The MaxEnt (Maximum Entropy) workflow is generated in an extra EntryArchive IF both
69
+ the DMFT SinglePoint and the MaxEnt SinglePoint EntryArchives are present in the upload.
70
+ """
71
+
72
+ method = SubSection(sub_section=DMFTPlusMaxEntMethod)
73
+
74
+ results = SubSection(sub_section=DMFTPlusMaxEntResults)
75
+
76
+ def normalize(self, archive, logger):
77
+ if not self.results: # creates Results section if not present
78
+ self.results = DMFTPlusMaxEntResults()
79
+
80
+ super().normalize(archive, logger)