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,505 @@
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 typing import List
19
+ import numpy as np
20
+
21
+ from nomad.datamodel.data import ArchiveSection
22
+ from nomad.metainfo import SubSection, Section, Quantity, Reference, Package
23
+ from nomad.datamodel.metainfo.common import FastAccess
24
+ from nomad.datamodel.metainfo.workflow import Workflow, Link, Task, TaskReference
25
+ from runschema.method import (
26
+ Method,
27
+ XCFunctional,
28
+ BasisSetContainer,
29
+ )
30
+ from runschema.system import System
31
+ from runschema.calculation import (
32
+ Calculation,
33
+ BandGap,
34
+ Dos,
35
+ BandStructure,
36
+ GreensFunctions,
37
+ MagneticShielding,
38
+ ElectricFieldGradient,
39
+ SpinSpinCoupling,
40
+ MagneticSusceptibility,
41
+ )
42
+
43
+
44
+ m_package = Package()
45
+
46
+
47
+ def resolve_difference(values):
48
+ delta_values = None
49
+
50
+ for n in range(-1, -len(values), -1):
51
+ a = values[n]
52
+ b = values[n - 1]
53
+ if a is None or b is None:
54
+ continue
55
+ delta_values = abs(a - b)
56
+ if delta_values != 0.0:
57
+ break
58
+
59
+ return delta_values
60
+
61
+
62
+ INPUT_SYSTEM_NAME = 'Input system'
63
+ OUTPUT_SYSTEM_NAME = 'Output system'
64
+ INPUT_METHOD_NAME = 'Input method'
65
+ INPUT_CALCULATION_NAME = 'Input calculation'
66
+ OUTPUT_CALCULATION_NAME = 'Output calculation'
67
+ WORKFLOW_METHOD_NAME = 'Workflow method'
68
+ WORKFLOW_RESULTS_NAME = 'Workflow results'
69
+
70
+
71
+ class SimulationWorkflowMethod(ArchiveSection):
72
+ m_def = Section(validate=False)
73
+
74
+ def normalize(self, archive, logger):
75
+ pass
76
+
77
+
78
+ class SimulationWorkflowResults(ArchiveSection):
79
+ m_def = Section(validate=False)
80
+
81
+ calculation_result_ref = Quantity(
82
+ type=Reference(Calculation.m_def),
83
+ shape=[],
84
+ description="""
85
+ Reference to calculation result. In the case of serial workflows, this corresponds
86
+ to the final step in the simulation. For the parallel case, it refers to the reference calculation.
87
+ """,
88
+ categories=[FastAccess],
89
+ )
90
+
91
+ n_calculations = Quantity(
92
+ type=int,
93
+ shape=[],
94
+ description="""
95
+ Number of calculations in workflow.
96
+ """,
97
+ )
98
+
99
+ calculations_ref = Quantity(
100
+ type=Reference(Calculation.m_def),
101
+ shape=['n_calculations'],
102
+ description="""
103
+ List of references to each calculation section in the simulation.
104
+ """,
105
+ )
106
+
107
+ def normalize(self, archive, logger):
108
+ calculations = []
109
+ try:
110
+ calculations = archive.run[0].calculation
111
+ except Exception:
112
+ return
113
+
114
+ if not calculations:
115
+ return
116
+
117
+ if not self.calculation_result_ref:
118
+ self.calculation_result_ref = calculations[-1]
119
+
120
+ if not self.calculations_ref:
121
+ self.calculations_ref = calculations
122
+
123
+
124
+ class SimulationWorkflow(Workflow):
125
+ method = SubSection(sub_section=SimulationWorkflowMethod)
126
+
127
+ results = SubSection(sub_section=SimulationWorkflowResults, categories=[FastAccess])
128
+
129
+ def normalize(self, archive, logger):
130
+ super().normalize(archive, logger)
131
+
132
+ self._calculations: List[Calculation] = []
133
+ self._systems: List[System] = []
134
+ self._methods: List[Method] = []
135
+ try:
136
+ self._calculations = archive.run[-1].calculation
137
+ self._systems = archive.run[-1].system
138
+ self._methods = archive.run[-1].method
139
+ except Exception:
140
+ logger.warning('System, method and calculation required for normalization.')
141
+ pass
142
+
143
+ if not self._calculations:
144
+ return
145
+
146
+ if not self.inputs:
147
+ if self._systems:
148
+ self.m_add_sub_section(
149
+ Workflow.inputs,
150
+ Link(name=INPUT_SYSTEM_NAME, section=self._systems[0]),
151
+ )
152
+
153
+ if self.method is not None:
154
+ self.m_add_sub_section(
155
+ Workflow.inputs,
156
+ Link(name=WORKFLOW_METHOD_NAME, section=self.method),
157
+ )
158
+
159
+ for link in self.inputs:
160
+ if isinstance(link.section, System):
161
+ self.input_structure = link.section
162
+ break
163
+
164
+ if not self.outputs:
165
+ if self._calculations:
166
+ self.m_add_sub_section(
167
+ Workflow.outputs,
168
+ Link(name=OUTPUT_CALCULATION_NAME, section=self._calculations[-1]),
169
+ )
170
+
171
+ if self.results is not None:
172
+ self.m_add_sub_section(
173
+ Workflow.outputs,
174
+ Link(name=WORKFLOW_RESULTS_NAME, section=self.results),
175
+ )
176
+
177
+ if not self.tasks:
178
+ current_step = 1
179
+ current_time = None
180
+ inputs = []
181
+ add_inputs, add_outputs = False, False
182
+ tasks = []
183
+ for n, calc in enumerate(self._calculations):
184
+ if calc.time_physical is None or calc.time_calculation is None:
185
+ # all calculation sections should have time info inorder to create workflow tasks
186
+ tasks = []
187
+ break
188
+
189
+ task = Task(outputs=[Link(name=OUTPUT_CALCULATION_NAME, section=calc)])
190
+ # successive tasks in serial if overlap in time duration exists
191
+ if current_time is None:
192
+ current_time = calc.time_physical
193
+ start_time = calc.time_physical - calc.time_calculation
194
+ if start_time and (
195
+ start_time > current_time or np.isclose(start_time, current_time)
196
+ ):
197
+ task.inputs = inputs
198
+ inputs = []
199
+ current_step += 1
200
+ else:
201
+ if tasks:
202
+ for input in tasks[-1].inputs:
203
+ if input.name == INPUT_METHOD_NAME:
204
+ continue
205
+ task.m_add_sub_section(Task.inputs, input)
206
+ if calc.method_ref:
207
+ task.m_add_sub_section(
208
+ Task.inputs,
209
+ Link(name=INPUT_METHOD_NAME, section=calc.method_ref),
210
+ )
211
+ if calc.system_ref:
212
+ inputs.append(Link(name=INPUT_SYSTEM_NAME, section=calc.system_ref))
213
+ task.m_add_sub_section(
214
+ Task.outputs,
215
+ Link(name=OUTPUT_SYSTEM_NAME, section=calc.system_ref),
216
+ )
217
+ else:
218
+ inputs.append(Link(name=INPUT_CALCULATION_NAME, section=calc))
219
+ task.name = f'Step {current_step}'
220
+ tasks.append(task)
221
+ current_time = max(current_time, calc.time_physical)
222
+ # add input if first calc in tasks
223
+ add_inputs = add_inputs or n == 0
224
+ # add output if last calc in tasks
225
+ add_outputs = add_outputs or n == len(self._calculations) - 1
226
+ for task in tasks:
227
+ # add workflow inputs to first parallel tasks
228
+ if task.name == 'Step 1' and add_inputs:
229
+ task.inputs.extend(
230
+ [input for input in self.inputs if input not in task.inputs]
231
+ )
232
+ # add outputs of last parallel tasks to workflow outputs
233
+ if task.name == f'Step {current_step}' and add_outputs:
234
+ self.outputs.extend(
235
+ [
236
+ output
237
+ for output in task.outputs
238
+ if output not in self.outputs
239
+ ]
240
+ )
241
+
242
+ self.tasks = tasks
243
+
244
+
245
+ class ParallelSimulation(SimulationWorkflow):
246
+ def normalize(self, archive, logger):
247
+ super().normalize(archive, logger)
248
+
249
+ if not self.tasks:
250
+ for n, calculation in enumerate(self._calculations):
251
+ inputs, outputs = (
252
+ [],
253
+ [Link(name=OUTPUT_CALCULATION_NAME, section=calculation)],
254
+ )
255
+ if self._calculations[n].system_ref:
256
+ inputs.append(
257
+ Link(
258
+ name=INPUT_SYSTEM_NAME,
259
+ section=self._calculations[n].system_ref,
260
+ )
261
+ )
262
+ elif len(self._calculations) == len(self._systems):
263
+ inputs.append(
264
+ Link(name=INPUT_SYSTEM_NAME, section=self._systems[n])
265
+ )
266
+ else:
267
+ continue
268
+ if self._calculations[n].method_ref:
269
+ inputs.append(
270
+ Link(
271
+ name=INPUT_METHOD_NAME,
272
+ section=self._calculations[n].method_ref,
273
+ )
274
+ )
275
+ elif len(self._calculations) == len(self._methods):
276
+ inputs.append(
277
+ Link(name=INPUT_METHOD_NAME, section=self._methods[n])
278
+ )
279
+ elif len(self._methods) == 1:
280
+ inputs.append(
281
+ Link(name=INPUT_METHOD_NAME, section=self._methods[0])
282
+ )
283
+ self.tasks.append(
284
+ Task(name=f'Calculation {n}', inputs=inputs, outputs=outputs)
285
+ )
286
+
287
+
288
+ class SerialSimulation(SimulationWorkflow):
289
+ def normalize(self, archive, logger):
290
+ super().normalize(archive, logger)
291
+
292
+ if not self.tasks:
293
+ previous_structure = None
294
+ for n, calculation in enumerate(self._calculations):
295
+ inputs, outputs = (
296
+ [],
297
+ [Link(name=OUTPUT_CALCULATION_NAME, section=calculation)],
298
+ )
299
+ if calculation.system_ref:
300
+ input_structure = (
301
+ self.input_structure
302
+ if n == 0
303
+ else self._calculations[n - 1].system_ref
304
+ )
305
+ if not input_structure:
306
+ input_structure = previous_structure
307
+ if input_structure:
308
+ inputs.append(
309
+ Link(name=INPUT_SYSTEM_NAME, section=input_structure)
310
+ )
311
+ previous_structure = calculation.system_ref
312
+ outputs.append(
313
+ Link(name=OUTPUT_SYSTEM_NAME, section=calculation.system_ref)
314
+ )
315
+ elif len(self._calculations) == len(self._systems):
316
+ inputs.append(
317
+ Link(
318
+ name=INPUT_SYSTEM_NAME,
319
+ section=self.input_structure
320
+ if n == 0
321
+ else self._systems[n - 1],
322
+ )
323
+ )
324
+ outputs.append(
325
+ Link(name=OUTPUT_SYSTEM_NAME, section=self._systems[n])
326
+ )
327
+ else:
328
+ continue
329
+ if calculation.method_ref:
330
+ inputs.append(
331
+ Link(name=INPUT_METHOD_NAME, section=calculation.method_ref)
332
+ )
333
+ elif len(self._calculations) == len(self._methods):
334
+ inputs.append(
335
+ Link(name=INPUT_METHOD_NAME, section=self._methods[n])
336
+ )
337
+ elif len(self._methods) == 1:
338
+ inputs.append(
339
+ Link(name=INPUT_METHOD_NAME, section=self._methods[0])
340
+ )
341
+ self.tasks.append(
342
+ Task(name=f'Step {n}', inputs=inputs, outputs=outputs)
343
+ )
344
+
345
+
346
+ class BeyondDFT(SerialSimulation):
347
+ """
348
+ Base class used to normalize standard workflows beyond DFT containing two specific
349
+ SinglePoint tasks (GWWorkflow = DFT + GW, DMFTWorkflow = DFT + DMFT,
350
+ MaxEntWorkflow = DMFT + MaxEnt, and so on) and store the outputs in the self.results
351
+ section.
352
+ """
353
+
354
+ def _resolve_outputs_section(self, output_section, task: TaskReference) -> None:
355
+ """
356
+ Resolves the output_section of a task and stores the results in the output_section.
357
+
358
+ Args:
359
+ task (TaskReference): The task from which the outputs are got.
360
+ """
361
+ for name, section in output_section.m_def.all_quantities.items():
362
+ name = f'{name}_electronic' if name in ['dos', 'band_structure'] else name
363
+ try:
364
+ calc_section = getattr(task.outputs[-1].section, name)
365
+ if calc_section:
366
+ output_section.m_set(section, calc_section)
367
+ except Exception:
368
+ continue
369
+
370
+ def get_electronic_structure_workflow_results(self, task_map: dict) -> None:
371
+ """
372
+ Gets the standard electronic structure workflow results section by resolving the
373
+ outputs specified in the `task_map`.
374
+
375
+ Args:
376
+ task_map (dict): The dictionary used to resolve the outputs sections.
377
+ """
378
+ for method, task in task_map.items():
379
+ outputs = ElectronicStructureOutputs()
380
+ self._resolve_outputs_section(outputs, task)
381
+ setattr(self.results, f'{method}_outputs', outputs)
382
+
383
+ def normalize(self, archive, logger):
384
+ super().normalize(archive, logger)
385
+
386
+ if len(self.tasks) != 2:
387
+ logger.error('Expected two tasks.')
388
+ return
389
+
390
+ # We extract the workflow name from the tasks names
391
+ self.name = '+'.join([task.name for task in self.tasks if task.name])
392
+ task_map = {
393
+ task.name.lower(): self.tasks[n] for n, task in enumerate(self.tasks)
394
+ }
395
+ # Resolve workflow2.results for each standard BeyondDFT workflow
396
+ if self.name == 'DFT+GW':
397
+ self.get_electronic_structure_workflow_results(task_map)
398
+ elif self.name == 'TB+DMFT': # TODO extend for DFT tasks
399
+ self.get_electronic_structure_workflow_results(task_map)
400
+ elif self.name == 'DMFT+MaxEnt':
401
+ self.get_electronic_structure_workflow_results(task_map)
402
+ elif self.name == 'FirstPrinciples+TB':
403
+ task_map['first_principles'] = task_map.pop('firstprinciples')
404
+ self.get_electronic_structure_workflow_results(task_map)
405
+
406
+
407
+ class DFTMethod(SimulationWorkflowMethod):
408
+ """
409
+ Base class defining the DFT input methodologies: starting XC functional and electrons
410
+ representation (basis set).
411
+ """
412
+
413
+ starting_point = Quantity(
414
+ type=Reference(XCFunctional),
415
+ description="""
416
+ Reference to the starting point (XC functional or HF) used.
417
+ """,
418
+ )
419
+
420
+ electrons_representation = Quantity(
421
+ type=Reference(BasisSetContainer),
422
+ description="""
423
+ Reference to the basis set used.
424
+ """,
425
+ )
426
+
427
+
428
+ class ElectronicStructureOutputs(SimulationWorkflowResults):
429
+ """
430
+ Base class defining the typical output properties of any electronic structure
431
+ SinglePoint calculation: DFT, TB, DMFT, GW, MaxEnt, XS.
432
+ """
433
+
434
+ band_gap = Quantity(
435
+ type=Reference(BandGap),
436
+ shape=['*'],
437
+ description="""
438
+ Reference to the band gap section.
439
+ """,
440
+ )
441
+
442
+ dos = Quantity(
443
+ type=Reference(Dos),
444
+ shape=['*'],
445
+ description="""
446
+ Reference to the density of states section.
447
+ """,
448
+ )
449
+
450
+ band_structure = Quantity(
451
+ type=Reference(BandStructure),
452
+ shape=['*'],
453
+ description="""
454
+ Reference to the band structure section.
455
+ """,
456
+ )
457
+
458
+ greens_functions = Quantity(
459
+ type=Reference(GreensFunctions),
460
+ shape=['*'],
461
+ description="""
462
+ Ref to the Green functions section.
463
+ """,
464
+ )
465
+
466
+
467
+ class MagneticOutputs(SimulationWorkflowResults):
468
+ """
469
+ Base class defining the typical output properties of magnetic SinglePoint calculations.
470
+ """
471
+
472
+ magnetic_shielding = Quantity(
473
+ type=Reference(MagneticShielding),
474
+ shape=['*'],
475
+ description="""
476
+ Reference to the magnetic shielding tensors.
477
+ """,
478
+ )
479
+
480
+ electric_field_gradient = Quantity(
481
+ type=Reference(ElectricFieldGradient),
482
+ shape=['*'],
483
+ description="""
484
+ Reference to the electric field gradient tensors.
485
+ """,
486
+ )
487
+
488
+ spin_spin_coupling = Quantity(
489
+ type=Reference(SpinSpinCoupling),
490
+ shape=['*'],
491
+ description="""
492
+ Reference to the spin-spin coupling tensors.
493
+ """,
494
+ )
495
+
496
+ magnetic_susceptibility_nmr = Quantity(
497
+ type=Reference(MagneticSusceptibility),
498
+ shape=['*'],
499
+ description="""
500
+ Reference to the magnetic susceptibility tensors.
501
+ """,
502
+ )
503
+
504
+
505
+ m_package.__init_metainfo__()