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 +13 -0
- qclab/algorithm.py +43 -0
- qclab/algorithms/__init__.py +8 -0
- qclab/algorithms/fewest_switches_surface_hopping.py +124 -0
- qclab/algorithms/mean_field.py +68 -0
- qclab/constants.py +37 -0
- qclab/data.py +186 -0
- qclab/dynamics/__init__.py +10 -0
- qclab/dynamics/dynamics.py +51 -0
- qclab/dynamics/parallel_driver_mpi.py +129 -0
- qclab/dynamics/parallel_driver_multiprocessing.py +104 -0
- qclab/dynamics/serial_driver.py +80 -0
- qclab/functions.py +844 -0
- qclab/ingredients.py +572 -0
- qclab/model.py +71 -0
- qclab/models/__init__.py +13 -0
- qclab/models/fmo_complex.py +117 -0
- qclab/models/holstein_lattice.py +191 -0
- qclab/models/spin_boson.py +85 -0
- qclab/models/tully_problem_one.py +131 -0
- qclab/models/tully_problem_three.py +124 -0
- qclab/models/tully_problem_two.py +126 -0
- qclab/numerical_constants.py +16 -0
- qclab/simulation.py +96 -0
- qclab/tasks/__init__.py +7 -0
- qclab/tasks/collect_tasks.py +105 -0
- qclab/tasks/initialization_tasks.py +447 -0
- qclab/tasks/update_tasks.py +1525 -0
- qclab/utils.py +107 -0
- qclab/variable.py +41 -0
- qclab-0.3.0a7.dist-info/LICENSE +201 -0
- qclab-0.3.0a7.dist-info/METADATA +297 -0
- qclab-0.3.0a7.dist-info/RECORD +35 -0
- qclab-0.3.0a7.dist-info/WHEEL +5 -0
- qclab-0.3.0a7.dist-info/top_level.txt +1 -0
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,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
|