qclab 0.3.0a7__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.
qclab/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ This module contains the QC Lab package initialization.
3
+ """
4
+
5
+ from qclab.simulation import Simulation
6
+ from qclab.variable import Variable
7
+ from qclab.data import Data
8
+ from qclab.model import Model
9
+ from qclab.algorithm import Algorithm
10
+ from qclab.constants import Constants
11
+ from qclab.utils import configure_memory_logger
12
+
13
+ configure_memory_logger()
qclab/algorithm.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ This module contains the Algorithm class.
3
+ """
4
+
5
+ import copy
6
+ from qclab.constants import Constants
7
+
8
+
9
+ class Algorithm:
10
+ """
11
+ Algorithm class for defining and executing algorithm recipes.
12
+ """
13
+
14
+ def __init__(self, default_settings=None, settings=None):
15
+ if settings is None:
16
+ settings = {}
17
+ if default_settings is None:
18
+ default_settings = {}
19
+ # Merge default settings with user-provided settings.
20
+ settings = {**default_settings, **settings}
21
+ # Construct a Constants object to hold settings.
22
+ self.settings = Constants()
23
+ # Put settings from the dictionary into the Constants object.
24
+ for key, val in settings.items():
25
+ setattr(self.settings, key, val)
26
+ # Copy the recipes and output variables to ensure they are not shared
27
+ # across instances.
28
+ self.initialization_recipe = copy.deepcopy(self.initialization_recipe)
29
+ self.update_recipe = copy.deepcopy(self.update_recipe)
30
+ self.collect_recipe = copy.deepcopy(self.collect_recipe)
31
+
32
+ initialization_recipe = []
33
+ update_recipe = []
34
+ collect_recipe = []
35
+
36
+ def execute_recipe(self, sim, state, parameters, recipe):
37
+ """
38
+ Carry out the given recipe for the simulation by running
39
+ each task in the recipe.
40
+ """
41
+ for func in recipe:
42
+ state, parameters = func(sim, state, parameters)
43
+ return state, parameters
@@ -0,0 +1,8 @@
1
+ """
2
+ This module imports the algorithm classes to qclab.algorithms.
3
+ """
4
+
5
+ from qclab.algorithms.mean_field import MeanField
6
+ from qclab.algorithms.fewest_switches_surface_hopping import (
7
+ FewestSwitchesSurfaceHopping,
8
+ )
@@ -0,0 +1,124 @@
1
+ """
2
+ This module contains the FewestSwitchesSurfaceHopping algorithm class.
3
+ """
4
+
5
+ from functools import partial
6
+ from qclab.algorithm import Algorithm
7
+ from qclab import tasks
8
+
9
+
10
+ class FewestSwitchesSurfaceHopping(Algorithm):
11
+ """
12
+ Fewest switches surface hopping algorithm class.
13
+ """
14
+
15
+ def __init__(self, settings=None):
16
+ if settings is None:
17
+ settings = {}
18
+ self.default_settings = {
19
+ "fssh_deterministic": False,
20
+ "gauge_fixing": "sign_overlap",
21
+ "use_gauge_field_force": False,
22
+ }
23
+ super().__init__(self.default_settings, settings)
24
+
25
+ initialization_recipe = [
26
+ tasks.initialize_variable_objects,
27
+ tasks.initialize_norm_factor,
28
+ tasks.initialize_branch_seeds,
29
+ tasks.initialize_z,
30
+ tasks.update_h_quantum,
31
+ partial(
32
+ tasks.diagonalize_matrix,
33
+ matrix_name="h_quantum",
34
+ eigvals_name="eigvals",
35
+ eigvecs_name="eigvecs",
36
+ ),
37
+ partial(
38
+ tasks.gauge_fix_eigs,
39
+ gauge_fixing="phase_der_couple",
40
+ eigvecs_previous_name="eigvecs",
41
+ ),
42
+ partial(tasks.copy_in_state, copy_name="eigvecs_previous", orig_name="eigvecs"),
43
+ partial(
44
+ tasks.basis_transform_vec,
45
+ input_vec_name="wf_db",
46
+ basis_name="eigvecs",
47
+ output_vec_name="wf_adb",
48
+ adb_to_db=False,
49
+ ),
50
+ tasks.initialize_random_values_fssh,
51
+ tasks.initialize_active_surface,
52
+ tasks.initialize_dm_adb_0_fssh,
53
+ tasks.update_act_surf_wf,
54
+ ]
55
+
56
+ update_recipe = [
57
+ partial(tasks.copy_in_state, copy_name="eigvecs_previous", orig_name="eigvecs"),
58
+ # Begin RK4 integration steps.
59
+ tasks.update_classical_forces,
60
+ partial(
61
+ tasks.update_quantum_classical_forces,
62
+ wf_db_name="act_surf_wf",
63
+ wf_changed=True,
64
+ ),
65
+ tasks.update_z_rk4_k1,
66
+ partial(tasks.update_classical_forces, z_name="z_1"),
67
+ partial(
68
+ tasks.update_quantum_classical_forces,
69
+ wf_db_name="act_surf_wf",
70
+ z_name="z_1",
71
+ wf_changed=False,
72
+ ),
73
+ tasks.update_z_rk4_k2,
74
+ partial(tasks.update_classical_forces, z_name="z_2"),
75
+ partial(
76
+ tasks.update_quantum_classical_forces,
77
+ wf_db_name="act_surf_wf",
78
+ z_name="z_2",
79
+ wf_changed=False,
80
+ ),
81
+ tasks.update_z_rk4_k3,
82
+ partial(tasks.update_classical_forces, z_name="z_3"),
83
+ partial(
84
+ tasks.update_quantum_classical_forces,
85
+ wf_db_name="act_surf_wf",
86
+ z_name="z_3",
87
+ wf_changed=False,
88
+ ),
89
+ tasks.update_z_rk4_k4,
90
+ # End RK4 integration steps.
91
+ tasks.update_wf_db_eigs,
92
+ tasks.update_h_quantum,
93
+ partial(
94
+ tasks.diagonalize_matrix,
95
+ matrix_name="h_quantum",
96
+ eigvals_name="eigvals",
97
+ eigvecs_name="eigvecs",
98
+ ),
99
+ tasks.gauge_fix_eigs,
100
+ partial(
101
+ tasks.basis_transform_vec,
102
+ input_vec_name="wf_db",
103
+ basis_name="eigvecs",
104
+ output_vec_name="wf_adb",
105
+ adb_to_db=False,
106
+ ),
107
+ tasks.update_hop_probs_fssh,
108
+ tasks.update_hop_inds_fssh,
109
+ tasks.update_hop_vals_fssh,
110
+ tasks.update_z_hop_fssh,
111
+ tasks.update_act_surf_hop_fssh,
112
+ tasks.update_act_surf_wf,
113
+ ]
114
+
115
+ collect_recipe = [
116
+ tasks.update_t,
117
+ tasks.update_dm_db_fssh,
118
+ tasks.update_quantum_energy_fssh,
119
+ tasks.update_classical_energy_fssh,
120
+ tasks.collect_t,
121
+ tasks.collect_dm_db,
122
+ tasks.collect_quantum_energy,
123
+ tasks.collect_classical_energy,
124
+ ]
@@ -0,0 +1,68 @@
1
+ """
2
+ This module contains the MeanField algorithm class.
3
+ """
4
+
5
+ from functools import partial
6
+ from qclab.algorithm import Algorithm
7
+ from qclab import tasks
8
+
9
+
10
+ class MeanField(Algorithm):
11
+ """
12
+ Mean-field dynamics algorithm class.
13
+ """
14
+
15
+ def __init__(self, settings=None):
16
+ if settings is None:
17
+ settings = {}
18
+ self.default_settings = {}
19
+ super().__init__(self.default_settings, settings)
20
+
21
+ initialization_recipe = [
22
+ tasks.initialize_variable_objects,
23
+ tasks.initialize_norm_factor,
24
+ tasks.initialize_z,
25
+ tasks.update_h_quantum,
26
+ ]
27
+
28
+ update_recipe = [
29
+ # Begin RK4 integration steps.
30
+ partial(tasks.update_classical_forces, z_name="z"),
31
+ tasks.update_quantum_classical_forces,
32
+ tasks.update_z_rk4_k1,
33
+ partial(tasks.update_classical_forces, z_name="z_1"),
34
+ partial(
35
+ tasks.update_quantum_classical_forces,
36
+ z_name="z_1",
37
+ wf_changed=False,
38
+ ),
39
+ tasks.update_z_rk4_k2,
40
+ partial(tasks.update_classical_forces, z_name="z_2"),
41
+ partial(
42
+ tasks.update_quantum_classical_forces,
43
+ z_name="z_2",
44
+ wf_changed=False,
45
+ ),
46
+ tasks.update_z_rk4_k3,
47
+ partial(tasks.update_classical_forces, z_name="z_3"),
48
+ partial(
49
+ tasks.update_quantum_classical_forces,
50
+ z_name="z_3",
51
+ wf_changed=False,
52
+ ),
53
+ tasks.update_z_rk4_k4,
54
+ # End RK4 integration steps.
55
+ tasks.update_wf_db_rk4,
56
+ tasks.update_h_quantum,
57
+ ]
58
+
59
+ collect_recipe = [
60
+ tasks.update_t,
61
+ tasks.update_dm_db_mf,
62
+ tasks.update_quantum_energy,
63
+ tasks.update_classical_energy,
64
+ tasks.collect_t,
65
+ tasks.collect_dm_db,
66
+ tasks.collect_classical_energy,
67
+ tasks.collect_quantum_energy,
68
+ ]
qclab/constants.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ This module contains the Constants class.
3
+ """
4
+
5
+
6
+ class Constants:
7
+ """
8
+ Constants class to define constants and trigger updates when they change.
9
+ """
10
+
11
+ def __init__(self, update_function=None):
12
+ self._updating = False
13
+ self._init_complete = False
14
+ self._update_function = update_function
15
+
16
+ def __setattr__(self, name, value):
17
+ """
18
+ Overrides attribute setting to call the update function after the attribute is
19
+ changed, preventing recursion.
20
+ """
21
+ super().__setattr__(name, value)
22
+ if not self._updating and name not in {
23
+ "_updating",
24
+ "_update_function",
25
+ "_init_complete",
26
+ }:
27
+ if self._init_complete:
28
+ self._updating = True
29
+ if self._update_function is not None:
30
+ self._update_function()
31
+ self._updating = False
32
+
33
+ def get(self, name, default=None):
34
+ """
35
+ Get the value of an attribute, returning default if it does not exist.
36
+ """
37
+ return getattr(self, name, default)
qclab/data.py ADDED
@@ -0,0 +1,186 @@
1
+ """
2
+ This module contains the Data class.
3
+ """
4
+
5
+ import logging
6
+ import numpy as np
7
+ from qclab.utils import DISABLE_H5PY
8
+
9
+ if not DISABLE_H5PY:
10
+ import h5py
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Data:
16
+ """
17
+ Data class for handling the collection of data during a simulation.
18
+ """
19
+
20
+ def __init__(self, seeds=None):
21
+ if seeds is None:
22
+ seeds = np.array([], dtype=int)
23
+ self.data_dict = {"seed": seeds, "norm_factor": 0}
24
+ # Store log messages captured during a simulation run. This attribute is
25
+ # populated by the drivers when they return the Data object.
26
+ self.log = ""
27
+
28
+ def add_output_to_data_dict(self, sim, state, t_ind):
29
+ """
30
+ Add data to the output dictionary ``self.data_dict``.
31
+
32
+ .. rubric:: Args
33
+ sim: Simulation
34
+ The simulation object containing settings and parameters.
35
+ state: Variable
36
+ The state object containing the current simulation state.
37
+ t_ind: int
38
+ The current time index in the simulation.
39
+ """
40
+ # Check if the norm_factor is zero. If it is, save it from the state object.
41
+ if self.data_dict["norm_factor"] == 0:
42
+ if not hasattr(state, "norm_factor"):
43
+ logger.critical(
44
+ "The state object does not have a norm_factor attribute. "
45
+ "This is required to normalize the data."
46
+ )
47
+ raise ValueError(
48
+ "The state object does not have a norm_factor attribute."
49
+ )
50
+ logger.info(
51
+ "Setting norm_factor to %s from state object.", state.norm_factor
52
+ )
53
+ self.data_dict["norm_factor"] = state.norm_factor
54
+ for key, val in state.output_dict.items():
55
+ if not key in self.data_dict:
56
+ # If the key is not in the data_dict, initialize it with zeros.
57
+ self.data_dict[key] = np.zeros(
58
+ (len(sim.settings.t_collect), *np.shape(val)[1:]), dtype=val.dtype
59
+ )
60
+ logger.info(
61
+ "Initializing data_dict[%s] with shape %s.",
62
+ key,
63
+ self.data_dict[key].shape,
64
+ )
65
+ # Store the data in the data_dict at the correct time index.
66
+ self.data_dict[key][t_ind // sim.settings.dt_collect_n] = (
67
+ np.sum(val, axis=0) / self.data_dict["norm_factor"]
68
+ )
69
+
70
+ def add_data(self, new_data):
71
+ """
72
+ Add data from ``new_data`` to the output dictionary ``self.data_dict``.
73
+
74
+ .. rubric:: Args
75
+ new_data: Data
76
+ A Data instance containing the new data to merge.
77
+ """
78
+ new_norm_factor = (
79
+ new_data.data_dict["norm_factor"] + self.data_dict["norm_factor"]
80
+ )
81
+ for key, val in new_data.data_dict.items():
82
+ if key == "seed":
83
+ self.data_dict[key] = np.concatenate(
84
+ (self.data_dict[key], val.flatten()), axis=0
85
+ )
86
+ elif key != "norm_factor":
87
+ if key in self.data_dict:
88
+ self.data_dict[key] = (
89
+ self.data_dict[key] * self.data_dict["norm_factor"]
90
+ + val * new_data.data_dict["norm_factor"]
91
+ ) / new_norm_factor
92
+ else:
93
+ self.data_dict[key] = val
94
+ self.data_dict["norm_factor"] = new_norm_factor
95
+ # Append any log messages stored in new_data to this instance's log.
96
+ if getattr(new_data, "log", ""):
97
+ self.log += new_data.log
98
+
99
+ def save(self, filename, disable_h5py=DISABLE_H5PY):
100
+ """
101
+ Save the data to disk with file name ``filename``.
102
+
103
+ If h5py is available the data is stored as an HDF5 archive; otherwise
104
+ each variable is saved using numpy.savez.
105
+
106
+ .. rubric:: Args
107
+ filename : str
108
+ The file name to save the data to.
109
+ disable_h5py : bool, default: qclab.utils.DISABLE_H5PY
110
+ If True, h5py is not used even if available.
111
+ """
112
+ if disable_h5py:
113
+ np.savez(filename, log=self.log, **self.data_dict)
114
+ else:
115
+ with h5py.File(filename, "w") as h5file:
116
+ self._recursive_save(h5file, "/", self.data_dict)
117
+ h5file.attrs["log"] = self.log
118
+
119
+ def load(self, filename, disable_h5py=DISABLE_H5PY):
120
+ """
121
+ Load a Data object from ``filename``.
122
+
123
+ .. rubric:: Args
124
+ filename : str
125
+ The file name to load the data from.
126
+ disable_h5py : bool, default: qclab.utils.DISABLE_H5PY
127
+ If True, h5py is not used even if available.
128
+
129
+ .. rubric:: Returns
130
+ Data : Data
131
+ The loaded Data object.
132
+ """
133
+ new_data = Data()
134
+ if disable_h5py:
135
+ loaded = np.load(filename, allow_pickle=True)
136
+ new_data.data_dict = {key: loaded[key] for key in loaded.files if key != "log"}
137
+ new_data.log = str(loaded.get("log", ""))
138
+ self.add_data(new_data)
139
+ return self
140
+ with h5py.File(filename, "r") as h5file:
141
+ new_data._recursive_load(h5file, "/", new_data.data_dict)
142
+ new_data.log = h5file.attrs["log"]
143
+ self.add_data(new_data)
144
+ return self
145
+
146
+ def _recursive_save(self, h5file, path, dict):
147
+ """
148
+ Recursively save dictionary contents to an HDF5 group.
149
+
150
+ .. rubric:: Args
151
+ h5file : h5py.File
152
+ The HDF5 file object to save data into.
153
+ path : str
154
+ The current path in the HDF5 file.
155
+ dict : dict
156
+ The dictionary to save.
157
+ """
158
+ for key, item in dict.items():
159
+ if isinstance(item, type(dict)):
160
+ self._recursive_save(h5file, path + key + "/", item)
161
+ else:
162
+ try:
163
+ h5file.create_dataset(path + key, data=np.asarray(item))
164
+ except TypeError as exc:
165
+ raise ValueError(
166
+ f"Cannot save {key} with unsupported type {type(item)}."
167
+ ) from exc
168
+
169
+ def _recursive_load(self, h5file, path, dict):
170
+ """
171
+ Recursively load dictionary contents from an HDF5 group.
172
+
173
+ .. rubric:: Args
174
+ h5file : h5py.File
175
+ The HDF5 file object to load data from.
176
+ path : str
177
+ The current path in the HDF5 file.
178
+ dict : dict
179
+ The dictionary to populate with loaded data.
180
+ """
181
+ for key, item in h5file[path].items():
182
+ if isinstance(item, h5py._hl.dataset.Dataset):
183
+ dict[key] = item[()]
184
+ elif isinstance(item, h5py._hl.group.Group):
185
+ dict[key] = {}
186
+ self._recursive_load(h5file, path + key + "/", dict[key])
@@ -0,0 +1,10 @@
1
+ """
2
+ This module imports the dynamics drivers to qclab.dynamics.
3
+ """
4
+
5
+ from qclab.dynamics.parallel_driver_multiprocessing import (
6
+ parallel_driver_multiprocessing,
7
+ )
8
+ from qclab.dynamics.parallel_driver_mpi import parallel_driver_mpi
9
+ from qclab.dynamics.serial_driver import serial_driver
10
+ from qclab.dynamics.dynamics import run_dynamics
@@ -0,0 +1,51 @@
1
+ """
2
+ This module contains the dynamics core.
3
+ """
4
+
5
+ import numpy as np
6
+ from tqdm import tqdm
7
+
8
+
9
+ def run_dynamics(sim, state, parameters, data):
10
+ """
11
+ Dynamics core for QC Lab.
12
+
13
+ .. rubric:: Args
14
+ sim: Simulation
15
+ The simulation object containing the model, algorithm, and settings.
16
+ state: Variable
17
+ The state variable object containing the simulation seeds.
18
+ parameters: Variable
19
+ The parameters variable object containing any additional parameters.
20
+ data: Data
21
+ The data object for collecting output data.
22
+
23
+ .. rubric:: Returns
24
+ data: Data
25
+ The updated data object containing collected output data.
26
+ """
27
+ # Define an update iterator using tqdm if progress_bar is True.
28
+ t_update_iterator = sim.settings.t_update_n
29
+ if getattr(sim.settings, "progress_bar", True):
30
+ t_update_iterator = tqdm(t_update_iterator)
31
+
32
+ # Iterate over each time step.
33
+ for sim.t_ind in t_update_iterator:
34
+ if sim.t_ind == 0:
35
+ # Execute initialization recipe.
36
+ state, parameters = sim.algorithm.execute_recipe(
37
+ sim, state, parameters, sim.algorithm.initialization_recipe
38
+ )
39
+ # Detect collect timesteps.
40
+ if np.mod(sim.t_ind, sim.settings.dt_collect_n) == 0:
41
+ # Calculate output variables.
42
+ state, parameters = sim.algorithm.execute_recipe(
43
+ sim, state, parameters, sim.algorithm.collect_recipe
44
+ )
45
+ # Collect totals in output dictionary.
46
+ data.add_output_to_data_dict(sim, state, sim.t_ind)
47
+ # Execute update recipe.
48
+ state, parameters = sim.algorithm.execute_recipe(
49
+ sim, state, parameters, sim.algorithm.update_recipe
50
+ )
51
+ return data
@@ -0,0 +1,129 @@
1
+ """
2
+ This module contains the parallel MPI driver.
3
+ """
4
+
5
+ import logging
6
+ import copy
7
+ import numpy as np
8
+ import qclab.dynamics as dynamics
9
+ from qclab.utils import get_log_output
10
+ from qclab import Variable, Data
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def parallel_driver_mpi(sim, seeds=None, data=None, num_tasks=None):
16
+ """
17
+ Parallel driver for the dynamics core using the mpi4py library.
18
+
19
+ .. rubric:: Args
20
+ sim: Simulation
21
+ The simulation object containing the model, algorithm, initial state, and settings.
22
+ seeds: ndarray, optional
23
+ An array of integer seeds for the trajectories. If None, seeds will be
24
+ generated automatically.
25
+ data: Data, optional
26
+ A Data object for collecting output data. If None, a new Data object
27
+ will be created.
28
+ num_tasks: int, optional
29
+ The number of tasks to use for parallel processing. If None, the
30
+ number of available tasks will be used.
31
+
32
+ .. rubric:: Returns
33
+ data: Data
34
+ The updated Data object containing collected output data.
35
+ """
36
+ # First initialize the model constants.
37
+ sim.model.initialize_constants()
38
+ try:
39
+ from mpi4py import MPI
40
+ except ImportError:
41
+ raise ImportError(
42
+ "The package mpi4py is required for the parallel_driver_mpi driver."
43
+ ) from None
44
+ except Exception as e:
45
+ raise RuntimeError(f"An error occurred when importing mpi4py: {e}") from None
46
+ if seeds is None:
47
+ offset = 0
48
+ if data is not None:
49
+ if len(data.data_dict["seed"]) > 0:
50
+ offset = np.max(data.data_dict["seed"]) + 1
51
+ else:
52
+ data = Data()
53
+ seeds = offset + np.arange(sim.settings.num_trajs, dtype=int)
54
+ num_trajs = sim.settings.num_trajs
55
+ else:
56
+ num_trajs = len(seeds)
57
+ logger.warning(
58
+ "Setting sim.settings.num_trajs to the number of provided seeds."
59
+ )
60
+ sim.settings.num_trajs = num_trajs
61
+ if data is None:
62
+ data = Data()
63
+ comm = MPI.COMM_WORLD
64
+ rank = comm.Get_rank()
65
+ if num_tasks is None:
66
+ size = comm.Get_size()
67
+ else:
68
+ size = num_tasks
69
+ # Determine the number of simulations required to execute the total number
70
+ # of trajectories.
71
+ if num_trajs % sim.settings.batch_size == 0:
72
+ num_sims = num_trajs // sim.settings.batch_size
73
+ else:
74
+ num_sims = num_trajs // sim.settings.batch_size + 1
75
+ batch_seeds_list = (
76
+ np.zeros((num_sims * sim.settings.batch_size), dtype=int) + np.nan
77
+ )
78
+ batch_seeds_list[:num_trajs] = seeds
79
+ batch_seeds_list = batch_seeds_list.reshape((num_sims, sim.settings.batch_size))
80
+ # Split the simulations into chunks for each MPI process.
81
+ chunk_inds = np.linspace(0, num_sims, size + 1, dtype=int)
82
+ start = chunk_inds[rank]
83
+ end = chunk_inds[rank + 1]
84
+ chunk_size = end - start
85
+ # Create the input data for each local simulation.
86
+ sim.initialize_timesteps()
87
+ local_input_data = [
88
+ (
89
+ copy.deepcopy(sim),
90
+ Variable(
91
+ {
92
+ "seed": batch_seeds_list[n][~np.isnan(batch_seeds_list[n])].astype(
93
+ int
94
+ )
95
+ }
96
+ ),
97
+ Variable(),
98
+ Data(batch_seeds_list[n][~np.isnan(batch_seeds_list[n])].astype(int)),
99
+ )
100
+ for n in np.arange(num_sims)[start:end]
101
+ ]
102
+ # Set the batch size for each local simulation.
103
+ for i in range(chunk_size):
104
+ local_input_data[i][0].settings.batch_size = len(local_input_data[i][1].seed)
105
+ # Execute the local simulations.
106
+ local_results = [dynamics.run_dynamics(*x) for x in local_input_data]
107
+ comm.Barrier()
108
+ # Collect results sequentially on rank 0.
109
+ tag_data, tag_done = 1, 2
110
+ if rank == 0:
111
+ for result in local_results:
112
+ data.add_data(result)
113
+ remaining = size - 1
114
+ status = MPI.Status()
115
+ while remaining:
116
+ msg = comm.recv(source=MPI.ANY_SOURCE, tag=MPI.ANY_TAG, status=status)
117
+ if status.Get_tag() == tag_done:
118
+ remaining -= 1
119
+ else:
120
+ data.add_data(msg)
121
+ else:
122
+ for result in local_results:
123
+ comm.send(result, dest=0, tag=tag_data)
124
+ comm.send(None, dest=0, tag=tag_done)
125
+ # Collect logs from all ranks and attach combined output on root rank.
126
+ gathered_logs = comm.gather(get_log_output(), root=0)
127
+ if rank == 0:
128
+ data.log = "".join(log for log in gathered_logs if log)
129
+ return data