Skip to content

Commit e0b5440

Browse files
Merge branch 'master' into new/flare-interface
2 parents 2be7966 + aa725e6 commit e0b5440

8 files changed

Lines changed: 165 additions & 84 deletions

File tree

.github/workflows/build.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

Modules/Cluster.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,30 @@ def __setattr__(self, name, value):
333333
super(Cluster, self).__setattr__(name, value)
334334

335335

336+
def __getstate__(self):
337+
"""
338+
Return the picklable state of the cluster.
339+
340+
The thread lock created by compute_ensemble_batch cannot be pickled,
341+
so it is dropped here. This allows sscha.Utilities.save_binary to
342+
store objects holding a cluster after a calculation has run.
343+
"""
344+
state = self.__dict__.copy()
345+
state["lock"] = None
346+
return state
347+
348+
349+
def __setstate__(self, state):
350+
"""
351+
Restore the cluster from a pickled state.
352+
353+
The thread lock is transient runtime state and is reset to None,
354+
as after __init__; compute_ensemble_batch recreates it when needed.
355+
"""
356+
state["lock"] = None
357+
self.__dict__.update(state)
358+
359+
336360

337361
def copy_file(self, source, destination, server_source = False, server_dest = True, raise_error=False, **kwargs):
338362
"""

Modules/SchaMinimizer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,11 @@ def minimization_step(self, custom_function_gradient = None, timer=None):
643643
else:
644644
self.dyn.Symmetrize(use_spglib = self.use_spglib)
645645

646+
# Enforce the dynamical matrix to be real at q = -q + G (time-reversal symmetry)
647+
bg = self.dyn.structure.get_reciprocal_vectors() / (2 * np.pi)
648+
for iq, q in enumerate(self.dyn.q_tot):
649+
if CC.Methods.get_min_dist_into_cell(bg, q, -q) < 1e-6:
650+
self.dyn.dynmats[iq] = np.real(self.dyn.dynmats[iq])
646651

647652
# If we have imaginary frequencies, force the kl ratio to zero
648653

Tutorials/H3S/Automatic_Calculations.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,21 +1197,21 @@
11971197
],
11981198
"metadata": {
11991199
"kernelspec": {
1200-
"display_name": "Python 3",
1200+
"display_name": "Python 3 (ipykernel)",
12011201
"language": "python",
12021202
"name": "python3"
12031203
},
12041204
"language_info": {
12051205
"codemirror_mode": {
12061206
"name": "ipython",
1207-
"version": 2
1207+
"version": 3
12081208
},
12091209
"file_extension": ".py",
12101210
"mimetype": "text/x-python",
12111211
"name": "python",
12121212
"nbconvert_exporter": "python",
12131213
"pygments_lexer": "ipython3",
1214-
"version": "3.7.6"
1214+
"version": "3.13.7"
12151215
}
12161216
},
12171217
"nbformat": 4,

tests/aiida_ensemble/test_aiida_ensemble.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
import pytest
33
import numpy as np
44

5+
try:
6+
import aiida
7+
HAVE_AIIDA = True
8+
except ImportError:
9+
HAVE_AIIDA = False
10+
511
from sscha.aiida_ensemble import AiiDAEnsemble
612

13+
aiida_required = pytest.mark.skipif(not HAVE_AIIDA, reason='aiida not installed')
14+
715

816
def get_ensemble() -> AiiDAEnsemble:
917
"""Return an AiiDAEnsemble instance."""
@@ -34,6 +42,7 @@ def test_clean_runs():
3442
assert np.all(np.isclose(ensemble.forces, np.ones((num_configs-1, num_atoms, 3))))
3543

3644

45+
@aiida_required
3746
@pytest.mark.usefixtures('aiida_profile')
3847
def test_get_running_workchains(generate_workchain_pw_node):
3948
"""Test the :func:`sscha.aiida_ensemble.get_running_workchains` method."""
@@ -67,6 +76,7 @@ def test_get_running_workchains(generate_workchain_pw_node):
6776
assert success == [False, True, False]
6877

6978

79+
@aiida_required
7080
@pytest.mark.usefixtures('aiida_profile')
7181
def test_submit_and_get_workchains(fixture_code):
7282
"""Test the :func:`sscha.aiida_ensemble.submit_and_get_workchains` method."""

tests/conftest.py

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@
1010

1111
import pytest
1212

13-
pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name
13+
# Conditionally include aiida fixtures only if aiida is available
14+
try:
15+
import aiida
16+
HAVE_AIIDA = True
17+
except ImportError:
18+
HAVE_AIIDA = False
19+
20+
if HAVE_AIIDA:
21+
pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name
22+
else:
23+
pytest_plugins = []
1424

1525

1626
@pytest.fixture(scope='session')
@@ -25,25 +35,27 @@ def filepath_tests():
2535

2636

2737
@pytest.fixture
28-
def filepath_fixtures(filepath_tests):
29-
"""Return the absolute filepath to the directory containing the file `fixtures`."""
30-
return os.path.join(filepath_tests, 'fixtures')
31-
32-
33-
@pytest.fixture(scope='function')
3438
def fixture_sandbox():
3539
"""Return a `SandboxFolder`."""
40+
if not HAVE_AIIDA:
41+
pytest.skip("aiida not installed")
3642
from aiida.common.folders import SandboxFolder
3743
with SandboxFolder() as folder:
3844
yield folder
3945

4046

41-
@pytest.fixture
42-
def fixture_localhost(aiida_localhost):
43-
"""Return a localhost `Computer`."""
44-
localhost = aiida_localhost
45-
localhost.set_default_mpiprocs_per_machine(1)
46-
return localhost
47+
if HAVE_AIIDA:
48+
@pytest.fixture
49+
def fixture_localhost(aiida_localhost):
50+
"""Return a localhost `Computer`."""
51+
localhost = aiida_localhost
52+
localhost.set_default_mpiprocs_per_machine(1)
53+
return localhost
54+
else:
55+
@pytest.fixture
56+
def fixture_localhost():
57+
"""Dummy fixture when aiida is not installed."""
58+
pytest.skip("aiida not installed")
4759

4860

4961
@pytest.fixture
@@ -131,49 +143,51 @@ def _serialize_builder(builder):
131143
return _serialize_builder
132144

133145

134-
@pytest.fixture(scope='session', autouse=True)
135-
def sssp(aiida_profile, generate_upf_data):
136-
"""Create an SSSP pseudo potential family from scratch."""
137-
from aiida.common.constants import elements
138-
from aiida.plugins import GroupFactory
146+
if HAVE_AIIDA:
139147

140-
aiida_profile.clear_profile()
148+
@pytest.fixture(scope='session', autouse=True)
149+
def sssp(aiida_profile, generate_upf_data):
150+
"""Create an SSSP pseudo potential family from scratch."""
151+
from aiida.common.constants import elements
152+
from aiida.plugins import GroupFactory
141153

142-
SsspFamily = GroupFactory('pseudo.family.sssp')
154+
aiida_profile.clear_profile()
143155

144-
cutoffs = {}
145-
stringency = 'standard'
156+
SsspFamily = GroupFactory('pseudo.family.sssp')
146157

147-
with tempfile.TemporaryDirectory() as dirpath:
148-
for values in elements.values():
158+
cutoffs = {}
159+
stringency = 'standard'
149160

150-
element = values['symbol']
161+
with tempfile.TemporaryDirectory() as dirpath:
162+
for values in elements.values():
151163

152-
actinides = ('Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr')
164+
element = values['symbol']
153165

154-
if element in actinides:
155-
continue
166+
actinides = ('Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr')
156167

157-
upf = generate_upf_data(element)
158-
dirpath = pathlib.Path(dirpath)
159-
filename = dirpath / f'{element}.upf'
168+
if element in actinides:
169+
continue
160170

161-
with open(filename, 'w+b') as handle:
162-
with upf.open(mode='rb') as source:
163-
handle.write(source.read())
164-
handle.flush()
171+
upf = generate_upf_data(element)
172+
dirpath = pathlib.Path(dirpath)
173+
filename = dirpath / f'{element}.upf'
165174

166-
cutoffs[element] = {
167-
'cutoff_wfc': 30.0,
168-
'cutoff_rho': 240.0,
169-
}
175+
with open(filename, 'w+b') as handle:
176+
with upf.open(mode='rb') as source:
177+
handle.write(source.read())
178+
handle.flush()
179+
180+
cutoffs[element] = {
181+
'cutoff_wfc': 30.0,
182+
'cutoff_rho': 240.0,
183+
}
170184

171-
label = 'SSSP/1.3/PBEsol/efficiency'
172-
family = SsspFamily.create_from_folder(dirpath, label)
185+
label = 'SSSP/1.3/PBEsol/efficiency'
186+
family = SsspFamily.create_from_folder(dirpath, label)
173187

174-
family.set_cutoffs(cutoffs, stringency, unit='Ry')
188+
family.set_cutoffs(cutoffs, stringency, unit='Ry')
175189

176-
return family
190+
return family
177191

178192

179193
@pytest.fixture

tests/test_gradient_evolution/test_cmp_gradients.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def compare_gradients(self, dyn_grad, struct_grad):
4242
ka = self.ka
4343

4444
if not os.path.exists("grad_{}.dat".format(ka)):
45-
np.savetxt("grad_{}.dat".format(ka), dyn_grad[0,:,:])
45+
np.savetxt("grad_{}.dat".format(ka), np.real(dyn_grad[0,:,:]))
4646
else:
4747
correct_grad = np.loadtxt("grad_{}.dat".format(ka))
4848
diff = np.max(np.abs(dyn_grad - correct_grad))
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import print_function
3+
from __future__ import division
4+
5+
import os
6+
import tempfile
7+
import threading
8+
9+
import cellconstructor as CC
10+
import cellconstructor.Phonons
11+
12+
import sscha
13+
import sscha.Cluster
14+
import sscha.Ensemble
15+
import sscha.Relax
16+
import sscha.SchaMinimizer
17+
import sscha.Utilities
18+
19+
"""
20+
Regression test for issue #114: save_binary failed with
21+
TypeError: cannot pickle '_thread.lock' object
22+
when the relax object holds a cluster that already ran a calculation
23+
(Cluster.compute_ensemble_batch stores a threading.Lock on the cluster).
24+
"""
25+
26+
27+
def test_save_binary_relax_with_cluster(verbose=False):
28+
total_path = os.path.dirname(os.path.abspath(__file__))
29+
os.chdir(total_path)
30+
31+
DATA_PATH = "../../Examples/ensemble_data_test/"
32+
33+
dyn = CC.Phonons.Phonons(os.path.join(DATA_PATH, "dyn"))
34+
35+
ens = sscha.Ensemble.Ensemble(dyn, 0, dyn.GetSupercell())
36+
ens.load(DATA_PATH, 2, 10)
37+
38+
minim = sscha.SchaMinimizer.SSCHA_Minimizer(ens)
39+
40+
cluster = sscha.Cluster.Cluster(hostname="localhost")
41+
relax = sscha.Relax.SSCHA(minim, N_configs=10, max_pop=2,
42+
cluster=cluster)
43+
44+
# Cluster.compute_ensemble_batch leaves a threading.Lock on the
45+
# cluster after the ensemble calculation; reproduce that state.
46+
relax.cluster.lock = threading.Lock()
47+
48+
with tempfile.TemporaryDirectory() as tmpdir:
49+
filename = os.path.join(tmpdir, "relax.bin")
50+
sscha.Utilities.save_binary(relax, filename)
51+
52+
loaded = sscha.Utilities.load_binary(filename)
53+
54+
# The lock is transient runtime state and must come back unset.
55+
assert loaded.cluster.lock is None
56+
assert loaded.cluster.hostname == "localhost"
57+
assert loaded.N_configs == relax.N_configs
58+
assert loaded.minim.ensemble.N == ens.N
59+
60+
if verbose:
61+
print("save_binary/load_binary round trip succeeded")
62+
63+
64+
if __name__ == "__main__":
65+
test_save_binary_relax_with_cluster(True)

0 commit comments

Comments
 (0)