Skip to content

Commit 6ab3ed1

Browse files
authored
Unstashing (#6826)
This commit provides the full functionality of unstashing. It implements: `UnstashCalculation` `UnstashTargetMode` `CalcJobState.UNSTASHING`
1 parent 494179d commit 6ab3ed1

File tree

13 files changed

+786
-140
lines changed

13 files changed

+786
-140
lines changed

docs/source/topics/calculations/usage.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ This calculation produces an ``aiida.in`` file in JSON format with the stashing
768768
769769
.. code-block:: none
770770
771-
{"working_directory": <orm.RemoteData>.get_remote_path(),
771+
{"source_path": <orm.RemoteData>.get_remote_path(),
772772
"source_list": ["aiida.out", "output.txt"],
773773
"target_base": "/path/to/stash"}
774774
@@ -783,14 +783,14 @@ Therefore, your script should parse the JSON, and implement the stashing by any
783783
.. code-block:: bash
784784
785785
json=$(cat)
786-
working_directory=$(echo "$json" | jq -r '.working_directory')
786+
source_path=$(echo "$json" | jq -r '.source_path')
787787
source_list=$(echo "$json" | jq -r '.source_list[]')
788788
target_base=$(echo "$json" | jq -r '.target_base')
789789
790790
mkdir -p "$target_base"
791791
for item in $source_list; do
792-
cp "$working_directory/$item" "$target_base/"
793-
echo "$working_directory/$item copied successfully."
792+
cp "$source_path/$item" "$target_base/"
793+
echo "$source_path/$item copied successfully."
794794
done
795795
796796
This way you can implement any custom logic in your script, such as tape commands, handling errors, or filtering files dynamically.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ requires-python = '>=3.9'
6868
'core.stash' = 'aiida.calculations.stash:StashCalculation'
6969
'core.templatereplacer' = 'aiida.calculations.templatereplacer:TemplatereplacerCalculation'
7070
'core.transfer' = 'aiida.calculations.transfer:TransferCalculation'
71+
'core.unstash' = 'aiida.calculations.unstash:UnstashCalculation'
7172

7273
[project.entry-points.'aiida.calculations.importers']
7374
'core.arithmetic.add' = 'aiida.calculations.importers.arithmetic.add:ArithmeticAddCalculationImporter'

src/aiida/calculations/stash.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class StashCalculation(CalcJob):
5454
5555
inputs = {
5656
'metadata': {
57-
'computer': Computer.collection.get(label="localhost"),
57+
'computer': load_computer(label="localhost"),
5858
'options': {
5959
'resources': {'num_machines': 1},
6060
'stash': {
@@ -64,7 +64,8 @@ class StashCalculation(CalcJob):
6464
},
6565
},
6666
},
67-
'source_node': node_1,
67+
'source_node': <RemoteData_NODE>,
68+
'code': <MY_CODE>
6869
}
6970
"""
7071

@@ -91,6 +92,7 @@ def define(cls, spec):
9192
'num_machines': 1,
9293
'num_mpiprocs_per_machine': 1,
9394
}
95+
9496
spec.inputs['metadata']['options']['input_filename'].default = 'aiida.in'
9597
spec.inputs['metadata']['options']['output_filename'].default = 'aiida.out'
9698

@@ -113,7 +115,7 @@ def prepare_for_submission(self, folder):
113115
if stash_mode == StashMode.SUBMIT_CUSTOM_CODE.value:
114116
with folder.open(self.options.input_filename, 'w', encoding='utf8') as handle:
115117
stash_dict = {
116-
'working_directory': self.inputs.source_node.get_remote_path(),
118+
'source_path': self.inputs.source_node.get_remote_path(),
117119
'source_list': self.inputs.metadata.options.stash.source_list,
118120
'target_base': self.inputs.metadata.options.stash.target_base,
119121
}

src/aiida/calculations/unstash.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
###########################################################################
2+
# Copyright (c), The AiiDA team. All rights reserved. #
3+
# This file is part of the AiiDA code. #
4+
# #
5+
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
6+
# For further information on the license, see the LICENSE.txt file #
7+
# For further information please visit http://www.aiida.net #
8+
###########################################################################
9+
"""Implementation of UnstashCalculation."""
10+
11+
import json
12+
from pathlib import Path
13+
14+
from aiida import orm
15+
from aiida.common import AIIDA_LOGGER
16+
from aiida.common.datastructures import CalcInfo, CodeInfo, UnstashTargetMode
17+
from aiida.engine import CalcJob
18+
19+
from .stash import StashCalculation
20+
21+
EXEC_LOGGER = AIIDA_LOGGER.getChild('UnstashCalculation')
22+
23+
24+
class UnstashCalculation(CalcJob):
25+
"""
26+
Utility to unstash files from a remote folder.
27+
28+
An example of how the input should look like:
29+
30+
.. code-block:: python
31+
32+
inputs = {
33+
'metadata': {
34+
'computer': Computer.collection.get(label="localhost"),
35+
'options': {
36+
'resources': {'num_machines': 1},
37+
'unstash': {
38+
'unstash_target_mode': UnstashTargetMode.NewNode.value,
39+
'source_list': ['aiida.in', '_aiidasubmit.sh'], # also accepts ['*']
40+
},
41+
},
42+
},
43+
'source_node': <RemoteStashData>,
44+
'code': <MY_CODE> # only in case of `type(source_node)==RemoteStashCustomData`
45+
}
46+
"""
47+
48+
def __init__(self, *args, **kwargs):
49+
super().__init__(*args, **kwargs)
50+
51+
@classmethod
52+
def define(cls, spec):
53+
super().define(spec)
54+
55+
spec.input(
56+
'source_node',
57+
valid_type=orm.RemoteStashData,
58+
required=True,
59+
help='',
60+
)
61+
62+
spec.inputs['metadata']['computer'].required = True
63+
spec.inputs['metadata']['options']['unstash'].required = True
64+
spec.inputs['metadata']['options']['unstash']['unstash_target_mode'].required = True
65+
spec.inputs['metadata']['options']['resources'].default = {
66+
'num_machines': 1,
67+
'num_mpiprocs_per_machine': 1,
68+
}
69+
70+
spec.inputs['metadata']['options']['input_filename'].default = 'aiida.in'
71+
spec.inputs['metadata']['options']['output_filename'].default = 'aiida.out'
72+
73+
def prepare_for_submission(self, folder):
74+
if self.inputs.source_node.computer.uuid != self.inputs.metadata.computer.uuid:
75+
EXEC_LOGGER.warning(
76+
'YOUR SETTING MIGHT RESULT IN A SILENT FAILURE!'
77+
' The computer of the source node and the computer of the calculation are strongly advised be the same.'
78+
' However, it is not mandatory,'
79+
' in order to support the case that original computer somehow is not usable, anymore.'
80+
' E.g. the original computer was configured for ``core.torque``, but the HPC has move to SLURM,'
81+
' so you had to create a new computer configured with ``core.slurm``,'
82+
" and you'll need a job submission to do this."
83+
)
84+
source_node = self.inputs.get('source_node')
85+
unstash_target_mode = self.inputs.metadata.options.unstash.get('unstash_target_mode')
86+
87+
calc_info = CalcInfo()
88+
89+
if isinstance(source_node, orm.RemoteStashCustomData):
90+
if unstash_target_mode == UnstashTargetMode.OriginalPlace.value:
91+
92+
def traverse(node_):
93+
for link in node_.base.links.get_incoming():
94+
if (isinstance(link.node, CalcJob) and not isinstance(link.node, StashCalculation)) or (
95+
isinstance(link.node, orm.RemoteData)
96+
):
97+
return link.node
98+
return traverse(link.node)
99+
return None
100+
101+
stashed_calculation_node = traverse(source_node)
102+
103+
if not stashed_calculation_node:
104+
raise ValueError(
105+
'Your stash node is not connected to any calcjob node, cannot find the source path.'
106+
)
107+
108+
target_path = stashed_calculation_node.get_remote_path()
109+
else: # UnstashTargetMode.NewRemoteData.value
110+
computer = self.inputs.metadata.get('computer')
111+
with computer.get_transport() as transport:
112+
remote_user = transport.whoami_async()
113+
remote_working_directory = computer.get_workdir().format(username=remote_user)
114+
115+
# The following line is set at calcjob::presubmit, but we need it here
116+
calc_info_uuid = str(self.node.uuid)
117+
118+
# This is normally done in execmanager::upload_calculation,
119+
# however unfortunatly is not modular and I had to copy-paste the logic here
120+
target_path = Path(remote_working_directory).joinpath(
121+
calc_info_uuid[:2], calc_info_uuid[2:4], calc_info_uuid[4:]
122+
)
123+
124+
with folder.open(self.options.input_filename, 'w', encoding='utf8') as handle:
125+
stash_dict = {
126+
'source_path': self.inputs.source_node.target_basepath,
127+
'source_list': self.inputs.metadata.options.unstash.get('source_list'),
128+
'target_base': str(target_path),
129+
}
130+
stash_json = json.dumps(stash_dict)
131+
handle.write(f'{stash_json}\n')
132+
133+
code_info = CodeInfo()
134+
code_info.stdin_name = self.options.input_filename
135+
code_info.stdout_name = self.options.output_filename
136+
137+
if 'code' in self.inputs:
138+
code_info.code_uuid = self.inputs.code.uuid
139+
else:
140+
raise ValueError(
141+
f"Input 'code' is required for `UnstashTargetMode.{UnstashTargetMode(unstash_target_mode)}` mode."
142+
)
143+
144+
calc_info.codes_info = [code_info]
145+
calc_info.retrieve_list = [self.options.output_filename]
146+
calc_info.local_copy_list = []
147+
calc_info.remote_copy_list = []
148+
calc_info.remote_symlink_list = []
149+
150+
else:
151+
if 'code' in self.inputs:
152+
raise ValueError(
153+
f"Input 'code' cannot be used for `UnstashTargetMode.{UnstashTargetMode(unstash_target_mode)}`"
154+
' mode. This UnStash mode is performed on the login node, '
155+
'no submission is planned therefore no code is needed.'
156+
)
157+
158+
calc_info.skip_submit = True
159+
160+
calc_info.codes_info = []
161+
calc_info.retrieve_list = []
162+
calc_info.local_copy_list = []
163+
calc_info.remote_copy_list = []
164+
calc_info.remote_symlink_list = []
165+
166+
return calc_info

src/aiida/common/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
'TestsNotAllowedError',
7979
'TransportTaskException',
8080
'UniquenessError',
81+
'UnstashTargetMode',
8182
'UnsupportedSpeciesError',
8283
'ValidationError',
8384
'create_callback',

src/aiida/common/datastructures.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from .extendeddicts import DefaultFieldsAttributeDict
1717

18-
__all__ = ('CalcInfo', 'CalcJobState', 'CodeInfo', 'CodeRunMode', 'StashMode')
18+
__all__ = ('CalcInfo', 'CalcJobState', 'CodeInfo', 'CodeRunMode', 'StashMode', 'UnstashTargetMode')
1919

2020

2121
class StashMode(Enum):
@@ -29,13 +29,21 @@ class StashMode(Enum):
2929
SUBMIT_CUSTOM_CODE = 'submit_custom_code'
3030

3131

32+
class UnstashTargetMode(Enum):
33+
"""Mode to use when unstashing files."""
34+
35+
OriginalPlace = 'OriginalPlace'
36+
NewRemoteData = 'NewRemoteData'
37+
38+
3239
class CalcJobState(Enum):
3340
"""The sub state of a CalcJobNode while its Process is in an active state (i.e. Running or Waiting)."""
3441

3542
UPLOADING = 'uploading'
3643
SUBMITTING = 'submitting'
3744
WITHSCHEDULER = 'withscheduler'
3845
STASHING = 'stashing'
46+
UNSTASHING = 'unstashing'
3947
RETRIEVING = 'retrieving'
4048
PARSING = 'parsing'
4149

0 commit comments

Comments
 (0)