aiida-chemshell 0.3.1__tar.gz → 0.3.2__tar.gz

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.
Files changed (34) hide show
  1. {aiida_chemshell-0.3.1/src/aiida_chemshell.egg-info → aiida_chemshell-0.3.2}/PKG-INFO +2 -1
  2. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/pyproject.toml +2 -2
  3. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/base.py +10 -5
  4. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/utils.py +82 -1
  5. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/parsers/base.py +11 -2
  6. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/batch_calculation.py +1 -10
  7. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/isolated_atoms.py +19 -70
  8. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/optimisation.py +9 -131
  9. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2/src/aiida_chemshell.egg-info}/PKG-INFO +2 -1
  10. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/SOURCES.txt +0 -3
  11. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/requires.txt +1 -0
  12. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_inputs.py +46 -0
  13. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_isolated_atoms_workchain.py +42 -6
  14. aiida_chemshell-0.3.1/src/aiida_chemshell/calculations/file_conversion.py +0 -206
  15. aiida_chemshell-0.3.1/src/aiida_chemshell/calculations/solvant_removal.py +0 -115
  16. aiida_chemshell-0.3.1/src/aiida_chemshell/parsers/file_conversion.py +0 -54
  17. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/LICENSE +0 -0
  18. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/README.md +0 -0
  19. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/setup.cfg +0 -0
  20. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/__init__.py +0 -0
  21. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/calculations/__init__.py +0 -0
  22. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/parsers/__init__.py +0 -0
  23. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/periodic_table.py +0 -0
  24. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/units.py +0 -0
  25. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/utils.py +0 -0
  26. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/__init__.py +0 -0
  27. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell/workflows/utils.py +0 -0
  28. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/dependency_links.txt +0 -0
  29. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/entry_points.txt +0 -0
  30. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/src/aiida_chemshell.egg-info/top_level.txt +0 -0
  31. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_batch_workcahin.py +0 -0
  32. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_calculations.py +0 -0
  33. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_error_codes.py +0 -0
  34. {aiida_chemshell-0.3.1 → aiida_chemshell-0.3.2}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiida-chemshell
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: AiiDA workflow plugin for the ChemShell chemical modelling software package
5
5
  Author-email: "Dr. Benjamin T. Speake" <benjamin.speake@stfc.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: aiida-core>=2.5
45
+ Requires-Dist: ase>=3.29
45
46
  Provides-Extra: dev
46
47
  Requires-Dist: aiida-core>=2.8; extra == "dev"
47
48
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -3,13 +3,13 @@ requires = ["setuptools"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
- version = "0.3.1"
6
+ version = "0.3.2"
7
7
  name = "aiida-chemshell"
8
8
  description = "AiiDA workflow plugin for the ChemShell chemical modelling software package"
9
9
  authors = [
10
10
  { name = "Dr. Benjamin T. Speake", email = "benjamin.speake@stfc.ac.uk" },
11
11
  ]
12
- dependencies = ["aiida-core >= 2.5"]
12
+ dependencies = ["aiida-core >= 2.5", "ase>=3.29"]
13
13
  readme = "README.md"
14
14
  license = { file = "LICENSE" }
15
15
  keywords = ["aiida", "workflows", "chemshell"]
@@ -236,7 +236,10 @@ class ChemShellCalculation(CalcJob):
236
236
  "vibrational_modes",
237
237
  valid_type=ArrayData,
238
238
  required=False,
239
- help="The calculated vibrational modes of the system.",
239
+ help=(
240
+ "The calculated vibrational modes of the system. A negative "
241
+ "frequency indicates an imaginary frequency."
242
+ ),
240
243
  )
241
244
 
242
245
  # Validate inputs namespace
@@ -839,6 +842,8 @@ class ChemShellCalculation(CalcJob):
839
842
  if "optimisation_parameters" in node.inputs:
840
843
  if node.inputs.optimisation_parameters.get("thermal", False):
841
844
  return "ChemShell_Vibrational_Frequencies" + theory_key
845
+ if node.inputs.optimisation_parameters.get("neb", "no") != "no":
846
+ return "ChemShell_NEB" + theory_key
842
847
  return "ChemShell_Geometry_Optimisation" + theory_key
843
848
 
844
849
  return "ChemShell_Single_Point_Calculation" + theory_key
@@ -888,7 +893,6 @@ class ChemShellCalculation(CalcJob):
888
893
  fname = self.inputs.structure2.filename
889
894
  else:
890
895
  fname = ChemShellCalculation.FILE_TMP_STRUCTURE2
891
- print(fname)
892
896
  script += f"frg2 = Fragment(coords='{fname:s}')\n"
893
897
 
894
898
  ## Setup Theory objects
@@ -1016,8 +1020,9 @@ class ChemShellCalculation(CalcJob):
1016
1020
  node = self.inputs[port_name]
1017
1021
  if not node.label:
1018
1022
  node.label = label
1019
- if not node.description:
1020
- node.description = description
1023
+ if not node.description:
1024
+ node.description = description
1025
+ return
1021
1026
 
1022
1027
  def prepare_for_submission(self, folder: Folder) -> CalcInfo:
1023
1028
  """
@@ -1101,7 +1106,7 @@ class ChemShellCalculation(CalcJob):
1101
1106
  # Copy data for second input fragment if required
1102
1107
  if "structure2" in self.inputs:
1103
1108
  if isinstance(self.inputs.structure2, StructureData):
1104
- with folder.open(ChemShellCalculation.FILE_TMP_STRUCTURE, "wb") as f:
1109
+ with folder.open(ChemShellCalculation.FILE_TMP_STRUCTURE2, "wb") as f:
1105
1110
  f.write(self.inputs.structure2._prepare_xyz()[0])
1106
1111
  else:
1107
1112
  calc_info.local_copy_list.append(
@@ -15,7 +15,88 @@ from aiida.orm import (
15
15
 
16
16
 
17
17
  @calcfunction
18
- def create_dictionary(atoms, energies) -> Dict:
18
+ def create_isolated_atom_structures(structure) -> dict[str, StructureData]:
19
+ """
20
+ Create an isolated single-atom StructureData for each unique atom type.
21
+
22
+ The unique atom types present in ``structure`` are determined and, for each,
23
+ a non-periodic StructureData node containing a single atom at the origin is
24
+ created.
25
+
26
+ Parameters
27
+ ----------
28
+ structure : StructureData | SinglefileData
29
+ The input structure to extract isolated atomic species from. Either a
30
+ StructureData node or a SinglefileData node wrapping a structure file in
31
+ any format readable by ``ase.io.read``.
32
+
33
+ Returns
34
+ -------
35
+ dict[str, StructureData]
36
+ A mapping of atom type to its isolated single-atom StructureData node.
37
+ As a calcfunction return value, this is registered as a namespace of
38
+ output nodes, with the atom type used as the output link label.
39
+
40
+ Raises
41
+ ------
42
+ Exception
43
+ If the structure file format is not supported by the ASE reader.
44
+ """
45
+ unique_atoms = _determine_unique_atoms(structure)
46
+ structures = {}
47
+ for atom_symbol in unique_atoms:
48
+ atom = StructureData()
49
+ atom.append_atom(position=(0.0, 0.0, 0.0), symbols=atom_symbol)
50
+ atom.set_pbc((False, False, False))
51
+ atom.label = f"{atom_symbol} atom"
52
+ atom.description = (
53
+ f"Isolated {atom_symbol} atom extracted from Node: {structure.pk}"
54
+ )
55
+ structures[atom_symbol] = atom
56
+ return structures
57
+
58
+
59
+ def _determine_unique_atoms(structure: SinglefileData | StructureData) -> list[str]:
60
+ """Determine all unique atom types within the given structure."""
61
+ if isinstance(structure, StructureData):
62
+ return _atom_types_from_structuredata(structure)
63
+ return _atom_types_from_file(structure)
64
+
65
+
66
+ def _atom_types_from_structuredata(structure: StructureData) -> list[str]:
67
+ """Determine the unique atom types from a StructureData object."""
68
+ unique_atoms: list[str] = []
69
+ for site in structure.sites:
70
+ if site.kind_name not in unique_atoms:
71
+ unique_atoms.append(site.kind_name)
72
+ return unique_atoms
73
+
74
+
75
+ def _atom_types_from_file(structure: SinglefileData) -> list[str]:
76
+ """Determine the unique atom types from a SinglefileData object using ASE."""
77
+ import os
78
+ import tempfile
79
+
80
+ from ase.io import read
81
+ from ase.io.formats import UnknownFileTypeError
82
+
83
+ suffix = os.path.splitext(structure.filename)[1]
84
+ with tempfile.NamedTemporaryFile(suffix=suffix) as tmp:
85
+ with structure.open(mode="rb") as f:
86
+ tmp.write(f.read())
87
+ tmp.flush()
88
+ try:
89
+ atoms = read(tmp.name)
90
+ except UnknownFileTypeError as e:
91
+ raise Exception(
92
+ f"Unsupported structure file format for isolated atom extraction: "
93
+ f"{structure.filename}"
94
+ ) from e
95
+ return list(dict.fromkeys(atoms.get_chemical_symbols()))
96
+
97
+
98
+ @calcfunction
99
+ def create_atomic_energy_dictionary(atoms, energies) -> Dict:
19
100
  """Collate a series of isolated atom energies into a dictionary output."""
20
101
  if len(atoms) != len(energies):
21
102
  raise ValueError(
@@ -163,8 +163,17 @@ class ChemShellParser(Parser):
163
163
  elif "Mode" in line or "total" in line:
164
164
  pass
165
165
  else:
166
- # All remaining lines should be part of the modes table
167
- modes.append(numpy.array([float(x) for x in line_vals[2:]]))
166
+ # All remaining lines should be part of the modes table.
167
+ # Imaginary frequencies are printed with a trailing "i"
168
+ # (e.g. "100.0i") and are stored as the negative value.
169
+ modes.append(
170
+ numpy.array(
171
+ [
172
+ -float(x[:-1]) if x.endswith("i") else float(x)
173
+ for x in line_vals[2:]
174
+ ]
175
+ )
176
+ )
168
177
 
169
178
  if "Thermochemical analysis" in line:
170
179
  read = True
@@ -113,16 +113,7 @@ class BatchProcessWorkChain(WorkChain):
113
113
  def apply_default_input_tags(self) -> None:
114
114
  """Apply default labels/descriptions to WorkChain specific input nodes."""
115
115
  apply_default_input_node_tags(self.inputs, self.DEFAULT_INPUT_TAGS)
116
-
117
- # @classmethod
118
- # def validate_combination_input_key(cls, key: str | None, _) -> str | None:
119
- # """Validate the combine_results input key."""
120
- # if key in ["xyz", "trajectory"]:
121
- # return None
122
- # return (
123
- # f"Invalid input for 'combine_results'. {key} must be either 'xyz' or "
124
- # "'trajectory'"
125
- # )
116
+ return
126
117
 
127
118
  def validate_inputs(self):
128
119
  """Validate the inputs provided to the WorkChain."""
@@ -4,8 +4,10 @@ from aiida.engine import ToContext, WorkChain
4
4
  from aiida.orm import Dict, StructureData
5
5
  from aiida.plugins.factories import CalculationFactory
6
6
 
7
- from aiida_chemshell.calculations.utils import create_dictionary
8
- from aiida_chemshell.periodic_table import PeriodicTable
7
+ from aiida_chemshell.calculations.utils import (
8
+ create_atomic_energy_dictionary,
9
+ create_isolated_atom_structures,
10
+ )
9
11
  from aiida_chemshell.workflows.utils import apply_default_input_node_tags
10
12
 
11
13
  ChemShellCalculation = CalculationFactory("chemshell")
@@ -46,7 +48,7 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
46
48
 
47
49
  spec.outline(
48
50
  cls.apply_default_input_tags,
49
- cls.determine_unique_atom_types,
51
+ cls.create_atom_structures,
50
52
  cls.atom_energies,
51
53
  cls.result,
52
54
  )
@@ -57,31 +59,21 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
57
59
  """Apply default labels/descriptions to WorkChain specific input nodes."""
58
60
  apply_default_input_node_tags(self.inputs, self.DEFAULT_INPUT_TAGS)
59
61
 
60
- def determine_unique_atom_types(self) -> None:
61
- """Determine all unique atom types within the given structure."""
62
- self.unique_atoms = []
63
- if isinstance(self.inputs.structure, StructureData):
64
- self._atom_types_from_structuredata(self.inputs.structure)
65
- else:
66
- self._atom_types_from_file()
62
+ def create_atom_structures(self) -> None:
63
+ """Create provenance-tracked isolated atom structures for each atom type."""
64
+ if self.inputs.qm_parameters["theory"] == "PySCF":
65
+ raise Exception(
66
+ "Isolated atom calculations not supported by PySCF QM backend."
67
+ )
68
+ self.ctx.atom_structures = create_isolated_atom_structures(
69
+ self.inputs.structure
70
+ )
67
71
  return
68
72
 
69
73
  def atom_energies(self):
70
74
  """Run ChemShell single point calculations for each atom type."""
71
75
  calculations = {}
72
- if self.inputs.qm_parameters["theory"] == "PySCF":
73
- raise Exception(
74
- "Isolated atom calculations not supported by PySCF QM backend."
75
- )
76
- for atom_symbol in self.unique_atoms:
77
- structure = StructureData()
78
- structure.append_atom(position=(0.0, 0.0, 0.0), symbols=atom_symbol)
79
- structure.set_pbc((False, False, False))
80
- structure.label = f"{atom_symbol} atom"
81
- structure.description = (
82
- f"Isolated {atom_symbol} atom extracted from Node: "
83
- f"{self.inputs.structure.pk}"
84
- )
76
+ for atom_symbol, structure in self.ctx.atom_structures.items():
85
77
  inputs = {
86
78
  "structure": structure,
87
79
  "qm_parameters": self.inputs.qm_parameters,
@@ -95,9 +87,10 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
95
87
 
96
88
  def result(self) -> None:
97
89
  """Collect the results into a dictionary."""
98
- results_dict = create_dictionary(
99
- list(self.unique_atoms),
100
- [self.ctx.get(atom).outputs.energy for atom in self.unique_atoms],
90
+ unique_atoms = list(self.ctx.atom_structures.keys())
91
+ results_dict = create_atomic_energy_dictionary(
92
+ unique_atoms,
93
+ [self.ctx.get(atom).outputs.energy for atom in unique_atoms],
101
94
  )
102
95
  if isinstance(self.inputs.structure, StructureData):
103
96
  results_dict.label = (
@@ -111,47 +104,3 @@ class IsolatedAtomicEnergiesWorkChain(WorkChain):
111
104
  )
112
105
  self.out("atom_energies", results_dict)
113
106
  return
114
-
115
- def _atom_types_from_structuredata(self, structure: StructureData) -> None:
116
- """Determine the unique atom types from a StructureData object."""
117
- self.unique_atoms = []
118
- for site in structure.sites:
119
- if site.kind_name not in self.unique_atoms:
120
- self.unique_atoms.append(site.kind_name)
121
- return
122
-
123
- def _atom_types_from_file(self) -> None:
124
- """Determine the unique atom types from a SinglefileData object."""
125
- if self.inputs.structure.filename[-4:] == ".xyz":
126
- self._atom_types_from_xyz()
127
- elif self.inputs.structure.filename[-6:] == ".cjson":
128
- self._atom_types_from_cjson()
129
- elif self.inputs.structure.filename[-4:] == ".pun":
130
- self._atom_types_from_pun()
131
- return
132
-
133
- def _atom_types_from_xyz(self) -> None:
134
- """Determine the unique atom types from an xyz structure file."""
135
- structure = StructureData()
136
- structure._parse_xyz(self.inputs.structure.content.decode("utf-8"))
137
- self._atom_types_from_structuredata(structure)
138
- return
139
-
140
- def _atom_types_from_cjson(self) -> None:
141
- """Determine the unique atom types from a cjson structure file."""
142
- import json
143
-
144
- data = json.loads(self.inputs.structure.content).get("atoms").get("elements")
145
- atom_symbols: list[str] = data.get("symbol", [])
146
- atom_numbers: list[int] = data.get("number", [])
147
- if len(atom_symbols) == 0:
148
- self.unique_atoms = list(
149
- {PeriodicTable.atom_z_to_symbol(number) for number in atom_numbers}
150
- )
151
- else:
152
- self.unique_atoms = list(set(atom_symbols))
153
- return
154
-
155
- def _atom_types_from_pun(self) -> None:
156
- """Determine the unique atom types from a .pun structure file."""
157
- return
@@ -1,17 +1,14 @@
1
1
  """Workflows for geometry optimisation based taks."""
2
2
 
3
- from aiida.common.exceptions import MissingEntryPointError
4
3
  from aiida.engine import ToContext, WorkChain
5
4
  from aiida.orm import (
6
5
  ArrayData,
7
6
  Bool,
8
- Code,
9
7
  Dict,
10
8
  Float,
11
9
  SinglefileData,
12
10
  StructureData,
13
11
  )
14
- from aiida.plugins.factories import CalculationFactory
15
12
 
16
13
  from aiida_chemshell.calculations.base import ChemShellCalculation
17
14
  from aiida_chemshell.workflows.isolated_atoms import IsolatedAtomicEnergiesWorkChain
@@ -27,10 +24,6 @@ class GeometryOptimisationWorkChain(WorkChain):
27
24
  "Vibrational Analysis Flag",
28
25
  "Whether to calculate the vibrational modes of the optimised structure.",
29
26
  ),
30
- "mlip_model": (
31
- "MLIP Foundation Model",
32
- "The MLIP foundation model to apply fine-tuning to.",
33
- ),
34
27
  }
35
28
 
36
29
  @classmethod
@@ -71,48 +64,20 @@ class GeometryOptimisationWorkChain(WorkChain):
71
64
  "vibrational_modes",
72
65
  valid_type=ArrayData,
73
66
  required=False,
74
- help="The calculated vibrational modes for the optimised structure.",
67
+ help=(
68
+ "The calculated vibrational modes for the optimised structure. A "
69
+ "negative frequency indicates an imaginary frequency."
70
+ ),
75
71
  )
76
72
 
77
- # Optional inputs/outputs for using the results to fine tune a MLIP model
78
- # using the janus-core project via the aiida-mlip plugin.
79
- try:
80
- CalculationFactory("mlip.train")
81
- except MissingEntryPointError:
82
- pass
83
- else:
84
- from aiida_mlip.data.model import ModelData
85
-
86
- spec.input(
87
- "mlip_model",
88
- valid_type=ModelData,
89
- required=False,
90
- help="The MLIP foundation model to apply fine-tuning to.",
91
- )
92
- spec.input(
93
- "mlip_code",
94
- valid_type=Code,
95
- required=False,
96
- help="The Janus-core AiiDA code instance.",
97
- )
98
- spec.output(
99
- "fine_tuned_model", valid_type=ModelData, required=False, help=""
100
- )
101
- spec.output(
102
- "fine_tuned_model_compiled",
103
- valid_type=SinglefileData,
104
- required=False,
105
- help="",
106
- )
107
-
108
73
  ## Workflow ##
109
74
  spec.outline(
110
75
  cls.apply_default_input_tags,
111
76
  cls.optimise,
112
77
  cls.energy,
113
78
  cls.isolated_atom_energies,
114
- cls.generate_mlip_training_inputs,
115
- cls.train_mlip,
79
+ # cls.generate_mlip_training_inputs,
80
+ # cls.train_mlip,
116
81
  cls.result,
117
82
  )
118
83
 
@@ -146,6 +111,9 @@ class GeometryOptimisationWorkChain(WorkChain):
146
111
  if "mlip_model" in self.inputs:
147
112
  inputs["optimisation_parameters"]["save_path"] = True
148
113
 
114
+ if "maxcycle" not in inputs["optimisation_parameters"]:
115
+ inputs["optimisation_parameters"]["maxcycle"] = 300
116
+
149
117
  future = self.submit(ChemShellCalculation, **inputs)
150
118
  future.label = ChemShellCalculation.default_process_label(future)
151
119
  future.description = (
@@ -201,96 +169,6 @@ class GeometryOptimisationWorkChain(WorkChain):
201
169
  return ToContext(isolated_atoms=future)
202
170
  return None
203
171
 
204
- def generate_mlip_training_inputs(self):
205
- """Convert the optimisation path files to Janus compatible inputs."""
206
- if "mlip_model" in self.inputs:
207
- inputs = {
208
- "path": self.ctx.optimise.outputs.trajectory_path,
209
- "force": self.ctx.optimise.outputs.trajectory_force,
210
- "energies": self.ctx.optimise.outputs.optimisation_path,
211
- "atom_energies": self.ctx.isolated_atoms.outputs.atom_energies,
212
- "code": self.inputs.chemsh.code,
213
- "metadata": {
214
- "options": {
215
- "resources": {"num_mpiprocs_per_machine": 2, "num_machines": 1},
216
- # "withmpi": True,
217
- }
218
- },
219
- }
220
- from aiida_chemshell.calculations.file_conversion import (
221
- CreateJanusTrainingInputsCalcJob,
222
- )
223
-
224
- future = self.submit(CreateJanusTrainingInputsCalcJob, **inputs)
225
- future.label = "Generate MLIP training data set from geometry optimisation."
226
- future.description = (
227
- f"Data extraction step from WorkChainNode pk: {self.node.pk}"
228
- )
229
- return ToContext(create_mlip_inputs=future)
230
- return None
231
-
232
- def train_mlip(self):
233
- """Train a given MLIP model."""
234
- try:
235
- mlip_train_calc = CalculationFactory("mlip.train")
236
- except MissingEntryPointError:
237
- pass
238
- else:
239
- if "mlip_model" in self.inputs:
240
- # This needs to be properly addressed within aiida-mlip
241
- computer = self.inputs.get("mlip_code", None).computer
242
- work_dir = computer.get_workdir()
243
- with open("train.xyz", mode="wb") as f:
244
- f.write(self.ctx.create_mlip_inputs.outputs.training_input.content)
245
- with open("test.xyz", mode="wb") as f:
246
- f.write(self.ctx.create_mlip_inputs.outputs.test_input.content)
247
- with open("valid.xyz", mode="wb") as f:
248
- f.write(
249
- self.ctx.create_mlip_inputs.outputs.validation_input.content
250
- )
251
- with computer.get_transport() as transport:
252
- from pathlib import Path
253
-
254
- transport.putfile(
255
- Path("train.xyz").absolute(), f"{work_dir}/train.xyz"
256
- )
257
- transport.putfile(
258
- Path("test.xyz").absolute(), f"{work_dir}/test.xyz"
259
- )
260
- transport.putfile(
261
- Path("valid.xyz").absolute(), f"{work_dir}/valid.xyz"
262
- )
263
-
264
- import yaml
265
- from aiida_mlip.data.config import JanusConfigfile
266
-
267
- from aiida_chemshell.utils import generate_default_mlip_fine_tune_config
268
-
269
- config_dict = generate_default_mlip_fine_tune_config()
270
- config_dict["train_file"] = f"{work_dir}/train.xyz"
271
- config_dict["test_file"] = f"{work_dir}/test.xyz"
272
- config_dict["valid_file"] = f"{work_dir}/valid.xyz"
273
- config_dict["name"] = "ChemShell_Workflow_Test"
274
-
275
- with open("mlip_config.yml", "w+") as f:
276
- yaml.dump(config_dict, f, default_flow_style=False)
277
-
278
- mlip_inputs = {
279
- "mlip_config": JanusConfigfile(Path("mlip_config.yml").absolute()),
280
- "code": self.inputs.get("mlip_code", None),
281
- "fine_tune": True,
282
- "foundation_model": self.inputs.get("mlip_model", None),
283
- "metadata": {"options": {"resources": {"num_machines": 1}}},
284
- }
285
- # Submit the mlip training job
286
- future = self.submit(mlip_train_calc, **mlip_inputs)
287
- future.label = "MLIP Fine-Tuning."
288
- future.description = (
289
- f"MLIP fine-tuning step from WorkChainNode pk: {self.node.pk}"
290
- )
291
- return ToContext(mlip_training=future)
292
- return None
293
-
294
172
  def result(self):
295
173
  """Extract the final workflow results."""
296
174
  if "mlip_model" in self.inputs:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiida-chemshell
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: AiiDA workflow plugin for the ChemShell chemical modelling software package
5
5
  Author-email: "Dr. Benjamin T. Speake" <benjamin.speake@stfc.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: aiida-core>=2.5
45
+ Requires-Dist: ase>=3.29
45
46
  Provides-Extra: dev
46
47
  Requires-Dist: aiida-core>=2.8; extra == "dev"
47
48
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -13,12 +13,9 @@ src/aiida_chemshell.egg-info/requires.txt
13
13
  src/aiida_chemshell.egg-info/top_level.txt
14
14
  src/aiida_chemshell/calculations/__init__.py
15
15
  src/aiida_chemshell/calculations/base.py
16
- src/aiida_chemshell/calculations/file_conversion.py
17
- src/aiida_chemshell/calculations/solvant_removal.py
18
16
  src/aiida_chemshell/calculations/utils.py
19
17
  src/aiida_chemshell/parsers/__init__.py
20
18
  src/aiida_chemshell/parsers/base.py
21
- src/aiida_chemshell/parsers/file_conversion.py
22
19
  src/aiida_chemshell/workflows/__init__.py
23
20
  src/aiida_chemshell/workflows/batch_calculation.py
24
21
  src/aiida_chemshell/workflows/isolated_atoms.py
@@ -1,4 +1,5 @@
1
1
  aiida-core>=2.5
2
+ ase>=3.29
2
3
 
3
4
  [dev]
4
5
  aiida-core>=2.8
@@ -154,6 +154,52 @@ H 0.7546064020 0.5900323550 0.0000000000"""
154
154
  assert ChemShellCalculation.FILE_TMP_STRUCTURE in script_txt
155
155
 
156
156
 
157
+ def test_structure2_as_structuredata_object(
158
+ generate_calcjob, generate_inputs, water_structure_object
159
+ ):
160
+ """Test that a StructureData 'structure2' input is written to its own file.
161
+
162
+ Regression test: 'structure2' was previously written to the primary
163
+ structure's temporary file (FILE_TMP_STRUCTURE), overwriting it, instead
164
+ of its own file (FILE_TMP_STRUCTURE2).
165
+ """
166
+ structure2 = StructureData()
167
+ structure2.pbc = [False, False, False]
168
+ structure2.append_atom(symbols="O", position=[0.0, 0.0, 1.0])
169
+
170
+ inputs = generate_inputs(structure_fname=water_structure_object)
171
+ inputs["structure2"] = structure2
172
+
173
+ tmp_pth, calc_info = generate_calcjob(ChemShellCalculation, inputs)
174
+
175
+ structure_file = tmp_pth / ChemShellCalculation.FILE_TMP_STRUCTURE
176
+ structure2_file = tmp_pth / ChemShellCalculation.FILE_TMP_STRUCTURE2
177
+ assert structure_file.exists()
178
+ assert structure2_file.exists()
179
+
180
+ # The primary structure file must not have been overwritten by structure2.
181
+ chk_str = """3
182
+ Lattice="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" pbc="False False False"
183
+ O 0.0000000000 0.0000000000 0.0000000000
184
+ H -0.7546064020 0.5900323550 0.0000000000
185
+ H 0.7546064020 0.5900323550 0.0000000000"""
186
+ assert structure_file.read_text() == chk_str
187
+
188
+ chk_str2 = """1
189
+ Lattice="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" pbc="False False False"
190
+ O 0.0000000000 0.0000000000 1.0000000000"""
191
+ assert structure2_file.read_text() == chk_str2
192
+
193
+ script_file = tmp_pth / ChemShellCalculation.FILE_SCRIPT
194
+ assert script_file.exists()
195
+
196
+ script_txt = script_file.read_text()
197
+ assert (
198
+ f"frg2 = Fragment(coords='{ChemShellCalculation.FILE_TMP_STRUCTURE2:s}')\n"
199
+ in script_txt
200
+ )
201
+
202
+
157
203
  def test_cjson_to_structure_data(get_test_data_file):
158
204
  """Test parsing a ChemShell '.cjson' file into a StructureData node."""
159
205
  from aiida_chemshell.units import UnitsConverter
@@ -2,7 +2,6 @@
2
2
 
3
3
  from unittest.mock import MagicMock
4
4
 
5
- import pytest
6
5
  from aiida.engine import run_get_node
7
6
  from aiida.engine.utils import instantiate_process
8
7
  from aiida.manage.manager import get_manager
@@ -59,7 +58,7 @@ def test_metadata_options_forwarded_to_subcalculations(
59
58
  # Skip the actual (slow) ChemShell calculations; only capture the submissions.
60
59
  process.submit = MagicMock(return_value=MagicMock())
61
60
 
62
- process.determine_unique_atom_types()
61
+ process.create_atom_structures()
63
62
  process.atom_energies()
64
63
 
65
64
  # Water contains two unique atom types (O and H).
@@ -71,9 +70,47 @@ def test_metadata_options_forwarded_to_subcalculations(
71
70
  )
72
71
 
73
72
 
74
- @pytest.mark.xfail(reason="Will fail if NWChem not properly configured.")
75
- def test_geometry_optimisation_workflow(chemsh_code, get_test_data_file):
76
- """Test a geometry optimisation workflow with vibrational analysis."""
73
+ def test_isolated_atom_structures_are_provenance_tracked(
74
+ chemsh_code, water_structure_object
75
+ ):
76
+ """Isolated atom structures are created and correctly linked in provenance."""
77
+ from aiida.common.links import LinkType
78
+
79
+ inputs = {
80
+ "structure": water_structure_object,
81
+ "code": chemsh_code,
82
+ "qm_parameters": Dict({"theory": "NWChem"}),
83
+ }
84
+
85
+ runner = get_manager().get_runner()
86
+ process = instantiate_process(runner, IsolatedAtomicEnergiesWorkChain, **inputs)
87
+
88
+ process.create_atom_structures()
89
+
90
+ atom_structures = process.ctx.atom_structures
91
+ # Water contains two unique atom types (O and H).
92
+ assert set(atom_structures.keys()) == {"O", "H"}
93
+
94
+ for atom_structure in atom_structures.values():
95
+ assert atom_structure.is_stored, "Isolated atom structure was not stored."
96
+ creator = (
97
+ atom_structure.base.links.get_incoming(link_type=LinkType.CREATE).one().node
98
+ )
99
+ # The creator must be the calcfunction, which itself took the input
100
+ # structure as an input node.
101
+ input_pks = [
102
+ link.node.pk
103
+ for link in creator.base.links.get_incoming(
104
+ link_type=LinkType.INPUT_CALC
105
+ ).all()
106
+ ]
107
+ assert water_structure_object.pk in input_pks, (
108
+ "Isolated atom structure is not linked back to the input structure."
109
+ )
110
+
111
+
112
+ def test_full_workflow(chemsh_code, get_test_data_file):
113
+ """End-to-end test for the isolated atomic energies workflow."""
77
114
  inputs = {
78
115
  "structure": get_test_data_file(),
79
116
  "code": chemsh_code,
@@ -81,7 +118,6 @@ def test_geometry_optimisation_workflow(chemsh_code, get_test_data_file):
81
118
  }
82
119
  results, node = run_get_node(IsolatedAtomicEnergiesWorkChain, **inputs)
83
120
 
84
- # print(results)
85
121
  assert node.is_finished_ok, (
86
122
  "WorkChain node failed for IsolatedAtomicEnergiesWorkChain"
87
123
  )
@@ -1,206 +0,0 @@
1
- """CalcJob to create individual extended XYZ files for steps in an optimisation."""
2
-
3
- from aiida.common import CalcInfo, CodeInfo
4
- from aiida.common.folders import Folder
5
- from aiida.engine import CalcJob, CalcJobProcessSpec, PortNamespace
6
- from aiida.orm import ArrayData, Dict, SinglefileData, Str
7
-
8
- from aiida_chemshell.units import UnitsConverter
9
-
10
-
11
- class CreateJanusTrainingInputsCalcJob(CalcJob):
12
- """CalcJob to split an XYZ trajectory into individual ext XYZ files."""
13
-
14
- @classmethod
15
- def define(cls, spec: CalcJobProcessSpec) -> None:
16
- """Define the inputs, outputs and metadata for the CalcJob."""
17
- super().define(spec)
18
- spec.input(
19
- "path",
20
- valid_type=SinglefileData,
21
- required=True,
22
- help="An XYZ trajectory file containing the positions of each step.",
23
- )
24
- spec.input(
25
- "force",
26
- valid_type=SinglefileData,
27
- required=True,
28
- help="An XYZ trajectory file containing the forces at each step.",
29
- )
30
- spec.input(
31
- "energies",
32
- valid_type=ArrayData,
33
- required=True,
34
- help="The calculated dft energy of each step in the data series in a.u.",
35
- )
36
- spec.input(
37
- "atom_energies",
38
- valid_type=Dict,
39
- required=True,
40
- help="The isolated atomic energies for the training set.",
41
- )
42
-
43
- spec.input(
44
- "filename",
45
- valid_type=Str,
46
- required=False,
47
- help="The name to give the directory of output files.",
48
- )
49
-
50
- spec.output(
51
- "training_input",
52
- valid_type=SinglefileData,
53
- required=True,
54
- help="The main training data set in extended XYZ format.",
55
- )
56
- spec.output(
57
- "validation_input",
58
- valid_type=SinglefileData,
59
- required=True,
60
- help="The validation data set in extended XYZ format.",
61
- )
62
- spec.output(
63
- "test_input",
64
- valid_type=SinglefileData,
65
- required=True,
66
- help="The testing data set in extended XYZ format.",
67
- )
68
-
69
- ## Metadata
70
- spec.inputs["metadata"]["options"]["resources"].default = {
71
- "num_machines": 1,
72
- "num_mpiprocs_per_machine": 2,
73
- }
74
- spec.inputs["metadata"]["options"][
75
- "parser_name"
76
- ].default = "chemshell.file_conversion.mlip_training"
77
-
78
- # Exit Codes
79
- spec.exit_code(
80
- 300,
81
- "ERROR_NO_TRAJECTORY_FILES",
82
- message="No trajectory files have been produced.",
83
- )
84
-
85
- return
86
-
87
- @classmethod
88
- def validate_path_file(
89
- cls, value: SinglefileData, namepsace: PortNamespace
90
- ) -> str | None:
91
- """Perform validation checks on the input path xyz files."""
92
- if not isinstance(value, SinglefileData):
93
- return "Input trajectory needs to be a SinglefileData node."
94
- content = value.get_content("r")
95
- lines = content.split("\n")
96
- natoms = int(lines[0])
97
- if len(lines) % (natoms + 2) != 0:
98
- print(lines)
99
- return "Invalid XYZ trajectory structure detected."
100
- if len(lines) // (natoms + 2) < 5:
101
- return "Not enough individual configurations within input trajectory."
102
- return None
103
-
104
- def create_isolated_atom_energy_xyz(self) -> str:
105
- """Create the isolated atomistic energies in the required XYZ format."""
106
- xyz_str = ""
107
- zero = 0.0
108
- for atom in self.inputs.atom_energies.keys():
109
- xyz_str += "1\nProperties=species:S:1:pos:R:3:dft_forces:R:3 "
110
- energy = UnitsConverter.hartree_to_ev(self.inputs.atom_energies[atom])
111
- xyz_str += f"dft_energy={energy} "
112
- xyz_str += 'config_type=IsolatedAtom pbc="F F F"\n'
113
- xyz_str += f"{atom:8s} {zero:.4f} {zero:.4f} {zero:.4f} "
114
- xyz_str += f"{zero:.4f} {zero:.4f} {zero:.4f}\n"
115
- return xyz_str
116
-
117
- def prepare_for_submission(self, folder: Folder) -> CalcInfo:
118
- """Perform the python task to split the input trajectories."""
119
- script = self.generate_script()
120
- with folder.open("input.py", "w") as f:
121
- f.write(script)
122
-
123
- with folder.open("energies.txt", "w") as f:
124
- for val in self.inputs.energies.get_array("energies"):
125
- f.write(f"{UnitsConverter.hartree_to_ev(val):.10f}\n")
126
-
127
- with folder.open("isolated_atoms.xyz", "w") as f:
128
- f.write(self.create_isolated_atom_energy_xyz())
129
-
130
- code_info = CodeInfo()
131
- code_info.code_uuid = self.inputs.code.uuid
132
- if "chemsh.x" in str(self.inputs.code.filepath_executable):
133
- code_info.cmdline_params = [
134
- "input.py",
135
- ]
136
- else:
137
- n_machines = self.inputs.metadata.options.resources.get("num_machines")
138
- n_mpi_pm = self.inputs.metadata.options.resources.get(
139
- "num_mpiprocs_per_machine"
140
- )
141
- tot_mpi = n_machines * n_mpi_pm
142
- code_info.cmdline_params = [
143
- "-np",
144
- self.inputs.metadata.options.resources.get("tot_num_mpiprocs", tot_mpi),
145
- "input.py",
146
- ]
147
-
148
- calc_info = CalcInfo()
149
- calc_info.codes_info = [code_info]
150
- calc_info.provenance_exclude_list = []
151
- calc_info.retrieve_temporary_list = ["train.xyz", "valid.xyz", "test.xyz"]
152
- calc_info.local_copy_list = [
153
- (
154
- self.inputs.path.uuid,
155
- self.inputs.path.filename,
156
- self.inputs.path.filename,
157
- ),
158
- (
159
- self.inputs.force.uuid,
160
- self.inputs.force.filename,
161
- self.inputs.force.filename,
162
- ),
163
- ]
164
-
165
- return calc_info
166
-
167
- def generate_script(self) -> str:
168
- """Generate the python script for splitting the trajectory files."""
169
- return """
170
- with open("path.xyz", "r") as f:
171
- path = f.readlines()
172
- with open("path_force.xyz", "r") as f:
173
- force = f.readlines()
174
- energies = []
175
- with open("energies.txt", 'r') as f:
176
- for line in f:
177
- energies.append(float(line))
178
- natms = int(path[0])
179
- nsteps = len(path) // (natms + 2)
180
- valid_interval = 5
181
- test_interval = 10
182
- if nsteps < test_interval:
183
- test_interval = nsteps
184
- valid_interval = (nsteps // 2) + 1
185
- for step in range(nsteps):
186
- if (step + 1) % test_interval == 0:
187
- fname = "test.xyz"
188
- elif (step + 1) % valid_interval == 0:
189
- fname = "valid.xyz"
190
- else:
191
- fname = "train.xyz"
192
- with open(fname, "a") as f:
193
- f.write(f"{natms}\\n")
194
- f.write(f'Step={step} ')
195
- f.write(f'Properties=species:S:1:pos:R:3:dft_forces:R:3 pbc="F F F" ')
196
- f.write(f'energy={energies[step]}\\n')
197
- for i in range(2, natms + 2):
198
- index = (step * (natms + 2)) + i
199
- f.write(path[index].strip("\\n") + " ")
200
- f.write(" ".join(force[index].split()[1:]))
201
- f.write("\\n")
202
- with open("isolated_atoms.xyz", "r") as f:
203
- atoms = f.read()
204
- with open("train.xyz", "a") as f:
205
- f.write(atoms)
206
- """
@@ -1,115 +0,0 @@
1
- """Module for performing a solvant removal CalcJob."""
2
-
3
- from aiida.common import CalcInfo, CodeInfo
4
- from aiida.common.folders import Folder
5
- from aiida.engine import CalcJob, CalcJobProcessSpec
6
- from aiida.orm import Int, SinglefileData
7
-
8
- from aiida_chemshell.utils import xyz_file_validator
9
-
10
-
11
- class SolvantRemovalCalcJob(CalcJob):
12
- """CalcJob to extract a solvant from a complex for NEB calculations."""
13
-
14
- _SCRIPT_NAME = "separate_solvant.py"
15
-
16
- @classmethod
17
- def define(cls, spec: CalcJobProcessSpec) -> None:
18
- """Define the CalcJob spec for solvant removal."""
19
- super().define(spec)
20
- spec.input(
21
- "structure",
22
- valid_type=SinglefileData,
23
- required=True,
24
- validator=xyz_file_validator,
25
- help=(
26
- "Input structure file containing ligand+solvant gas phase complex "
27
- "in XYZ format."
28
- ),
29
- )
30
- spec.input(
31
- "num_ligand_atoms",
32
- valid_type=Int,
33
- required=True,
34
- help="Number of atoms within the core ligand molecule.",
35
- )
36
- spec.output(
37
- "unbound_structure",
38
- valid_type=SinglefileData,
39
- required=True,
40
- help="Gas phase structure for the unboun ligand+solvant complex.",
41
- )
42
-
43
- spec.inputs["metadata"]["options"]["resources"].default = {
44
- "num_machines": 1,
45
- "num_mpiprocs_per_machine": 1,
46
- }
47
- spec.inputs["metadata"]["options"]["parser_name"].default = "chemshell.unbind"
48
-
49
- def prepare_for_submission(self, folder: Folder) -> CalcInfo:
50
- """Prepare the CalcJob for submission."""
51
- py_scipt = self._generate_python_script()
52
- with folder.open(SolvantRemovalCalcJob._SCRIPT_NAME, "w") as handle:
53
- handle.write(py_scipt)
54
-
55
- code_info = CodeInfo()
56
- code_info.code_uuid = self.inputs.code.uuid
57
- code_info.cmdline_params = [SolvantRemovalCalcJob._SCRIPT_NAME]
58
-
59
- calc_info = CalcInfo()
60
- calc_info.codes_info = [code_info]
61
- calc_info.retrieve_temporary_list = []
62
- calc_info.provenance_exclude_list = []
63
- calc_info.retrieve_list = [self._generate_output_filename()]
64
- calc_info.local_copy_list = [
65
- (
66
- self.inputs.structure.uuid,
67
- self.inputs.structure.filename,
68
- self.inputs.structure.filename,
69
- ),
70
- ]
71
-
72
- return calc_info
73
-
74
- @classmethod
75
- def generate_output_filename(cls, input_filename) -> str:
76
- """Generate a filename based on the name of the initial structure file."""
77
- return input_filename.replace(".xyz", "_unbound.xyz")
78
-
79
- def _generate_output_filename(self) -> str:
80
- """Generate the output filename."""
81
- return self.generate_output_filename(self.inputs.structure.filename)
82
-
83
- def _generate_python_script(self) -> str:
84
- """Generate the Python script to be executed."""
85
- return f"""
86
- import numpy
87
- with open("{self.inputs.structure.filename}", "r") as f:
88
- lines = f.readlines()
89
- natoms = int(lines[0].strip())
90
- ligand = []
91
- for i in range(2, {int(self.inputs.num_ligand_atoms)} + 2):
92
- line = lines[i].split()
93
- ligand.append(numpy.array([float(line[1]), float(line[2]), float(line[3])]))
94
- solvant = []
95
- for i in range({int(self.inputs.num_ligand_atoms)} + 2, natoms + 2):
96
- line = lines[i].split()
97
- solvant.append(numpy.array([float(line[1]), float(line[2]), float(line[3])]))
98
- ligand_center = numpy.mean(ligand, axis=0)
99
- solvant_center = numpy.mean(solvant, axis=0)
100
- vector = solvant_center - ligand_center
101
- distance = numpy.linalg.norm(vector)
102
- vector /= distance
103
- displacement = vector * 10.0 # Move solvant 10 Angstroms away
104
- with open("{self._generate_output_filename()}", "w") as f:
105
- f.write(f"{{natoms}}\\n")
106
- f.write("Unbound ligand+solvant complex\\n")
107
- for i in range(2, {int(self.inputs.num_ligand_atoms)} + 2):
108
- f.write(lines[i])
109
- for i in range({int(self.inputs.num_ligand_atoms)} + 2, natoms + 2):
110
- line = lines[i].split()
111
- x = float(line[1]) + displacement[0]
112
- y = float(line[2]) + displacement[1]
113
- z = float(line[3]) + displacement[2]
114
- f.write(f"{{line[0]}} {{x:.8f}} {{y:.8f}} {{z:.8f}}\\n")
115
- """
@@ -1,54 +0,0 @@
1
- """Defines the parser for the split trajectory CalcJob."""
2
-
3
- import os
4
-
5
- from aiida.engine import ExitCode
6
- from aiida.orm import SinglefileData
7
- from aiida.parsers.parser import Parser
8
-
9
-
10
- class CreateJanusTrainingInputsParser(Parser):
11
- """AiiDA parser plugin for SplitTrajectory CalcJob."""
12
-
13
- def parse(self, **kwargs):
14
- """AiiDA parser plugin for SplitTrajectory CalcJob."""
15
- description_str = "MLIP {} data set extracted from ChemShell calculation."
16
- retrieved_temporary_folder = kwargs.get("retrieved_temporary_folder", None)
17
-
18
- # with self.retrieved.open("train.xyz", "r") as f:
19
- with open(os.path.join(retrieved_temporary_folder, "train.xyz"), "rb") as f:
20
- self.out(
21
- "training_input",
22
- SinglefileData(
23
- file=f,
24
- filename="train.xyz",
25
- label="MLIP training data.",
26
- description=description_str.format("training"),
27
- ),
28
- )
29
-
30
- # with self.retrieved.open("test.xyz", "r") as f:
31
- with open(os.path.join(retrieved_temporary_folder, "test.xyz"), "rb") as f:
32
- self.out(
33
- "test_input",
34
- SinglefileData(
35
- file=f,
36
- filename="test.xyz",
37
- label="MLIP test data.",
38
- description=description_str.format("testing"),
39
- ),
40
- )
41
-
42
- # with self.retrieved.open("valid.xyz", "r") as f:
43
- with open(os.path.join(retrieved_temporary_folder, "valid.xyz"), "rb") as f:
44
- self.out(
45
- "validation_input",
46
- SinglefileData(
47
- file=f,
48
- filename="valid.xyz",
49
- label="MLIP validation data.",
50
- description=description_str.format("validation"),
51
- ),
52
- )
53
-
54
- return ExitCode(0)
File without changes