Skip to content

Commit 31981d5

Browse files
authored
Retain Structure.properties in structure_from_abivars()/structure_to_abivars() round trip (#3552)
* retain Structure.properties in structure_from_abivars()/structure_to_abivars() round trip * test_structure.py add self.struct.properties["foo"] = "bar" for test_to_from_abivars()
1 parent 2ec68d7 commit 31981d5

File tree

5 files changed

+70
-75
lines changed

5 files changed

+70
-75
lines changed

pymatgen/core/structure.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,12 +2632,12 @@ def as_dataframe(self):
26322632
return df
26332633

26342634
@classmethod
2635-
def from_dict(cls, d: dict[str, Any], fmt: Literal["abivars"] | None = None) -> Structure:
2635+
def from_dict(cls, dct: dict[str, Any], fmt: Literal["abivars"] | None = None) -> Structure:
26362636
"""Reconstitute a Structure object from a dict representation of Structure
26372637
created using as_dict().
26382638
26392639
Args:
2640-
d (dict): Dict representation of structure.
2640+
dct (dict): Dict representation of structure.
26412641
fmt ('abivars' | None): Use structure_from_abivars() to parse the dict. Defaults to None.
26422642
26432643
Returns:
@@ -2646,12 +2646,12 @@ def from_dict(cls, d: dict[str, Any], fmt: Literal["abivars"] | None = None) ->
26462646
if fmt == "abivars":
26472647
from pymatgen.io.abinit.abiobjects import structure_from_abivars
26482648

2649-
return structure_from_abivars(cls=cls, **d)
2649+
return structure_from_abivars(cls=cls, **dct)
26502650

2651-
lattice = Lattice.from_dict(d["lattice"])
2652-
sites = [PeriodicSite.from_dict(sd, lattice) for sd in d["sites"]]
2653-
charge = d.get("charge")
2654-
return cls.from_sites(sites, charge=charge, properties=d.get("properties"))
2651+
lattice = Lattice.from_dict(dct["lattice"])
2652+
sites = [PeriodicSite.from_dict(sd, lattice) for sd in dct["sites"]]
2653+
charge = dct.get("charge")
2654+
return cls.from_sites(sites, charge=charge, properties=dct.get("properties"))
26552655

26562656
def to(self, filename: str | Path = "", fmt: str = "", **kwargs) -> str:
26572657
"""Outputs the structure to a file or string.

pymatgen/io/abinit/abiobjects.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ def lattice_from_abivars(cls=None, *args, **kwargs):
9595
raise ValueError(f"Don't know how to construct a Lattice from dict:\n{pformat(kwargs)}")
9696

9797

98-
def structure_from_abivars(cls=None, *args, **kwargs):
98+
def structure_from_abivars(cls=None, *args, **kwargs) -> Structure:
9999
"""
100100
Build a Structure object from a dictionary with ABINIT variables.
101101
102102
Args:
103-
cls: Structure class to be instantiated. pymatgen.core.structure.Structure if cls is None
103+
cls: Structure class to be instantiated. Defaults to Structure.
104104
105105
Example:
106106
al_structure = structure_from_abivars(
@@ -115,30 +115,28 @@ def structure_from_abivars(cls=None, *args, **kwargs):
115115
`xred` can be replaced with `xcart` or `xangst`.
116116
"""
117117
kwargs.update(dict(*args))
118-
d = kwargs
119-
120-
cls = Structure if cls is None else cls
118+
cls = cls or Structure
121119

122120
# lattice = Lattice.from_dict(d, fmt="abivars")
123-
lattice = lattice_from_abivars(**d)
124-
coords, coords_are_cartesian = d.get("xred"), False
121+
lattice = lattice_from_abivars(**kwargs)
122+
coords, coords_are_cartesian = kwargs.get("xred"), False
125123

126124
if coords is None:
127-
coords = d.get("xcart")
125+
coords = kwargs.get("xcart")
128126
if coords is not None:
129-
if "xangst" in d:
127+
if "xangst" in kwargs:
130128
raise ValueError("xangst and xcart are mutually exclusive")
131129
coords = ArrayWithUnit(coords, "bohr").to("ang")
132130
else:
133-
coords = d.get("xangst")
131+
coords = kwargs.get("xangst")
134132
coords_are_cartesian = True
135133

136134
if coords is None:
137-
raise ValueError(f"Cannot extract coordinates from:\n {d}")
135+
raise ValueError(f"Cannot extract coordinates from:\n {kwargs}")
138136

139137
coords = np.reshape(coords, (-1, 3))
140138

141-
znucl_type, typat = d["znucl"], d["typat"]
139+
znucl_type, typat = kwargs["znucl"], kwargs["typat"]
142140

143141
if not isinstance(znucl_type, Iterable):
144142
znucl_type = [znucl_type]
@@ -160,6 +158,7 @@ def structure_from_abivars(cls=None, *args, **kwargs):
160158
validate_proximity=False,
161159
to_unit_cell=False,
162160
coords_are_cartesian=coords_are_cartesian,
161+
properties=kwargs.get("properties"),
163162
)
164163

165164

@@ -199,11 +198,10 @@ def structure_to_abivars(structure, enforce_znucl=None, enforce_typat=None, **kw
199198
"""
200199
if not structure.is_ordered:
201200
raise ValueError(
202-
"""\
203-
Received disordered structure with partial occupancies that cannot be converted into an Abinit input.
204-
Please use OrderDisorderedStructureTransformation or EnumerateStructureTransformation
205-
to build an appropriate supercell from partial occupancies or, alternatively, use the Rigid Band Model
206-
or the Virtual Crystal Approximation."""
201+
"Received disordered structure with partial occupancies that cannot be converted into an "
202+
"Abinit input. Please use OrderDisorderedStructureTransformation or EnumerateStructureTransformation "
203+
"to build an appropriate supercell from partial occupancies or, alternatively, use the Rigid Band Model "
204+
"or the Virtual Crystal Approximation."
207205
)
208206

209207
n_atoms = len(structure)
@@ -227,7 +225,6 @@ def structure_to_abivars(structure, enforce_znucl=None, enforce_typat=None, **kw
227225
if not enforce_order:
228226
types_of_specie = species_by_znucl(structure)
229227

230-
# [ntypat] list
231228
znucl_type = [specie.number for specie in types_of_specie]
232229
typat = np.zeros(n_atoms, int)
233230
for atm_idx, site in enumerate(structure):
@@ -236,49 +233,50 @@ def structure_to_abivars(structure, enforce_znucl=None, enforce_typat=None, **kw
236233
znucl_type = enforce_znucl
237234
typat = enforce_typat
238235

239-
rprim = ArrayWithUnit(structure.lattice.matrix, "ang").to("bohr")
240-
angdeg = structure.lattice.angles
241-
xred = np.reshape([site.frac_coords for site in structure], (-1, 3))
236+
r_prim = ArrayWithUnit(structure.lattice.matrix, "ang").to("bohr")
237+
ang_deg = structure.lattice.angles
238+
x_red = np.reshape([site.frac_coords for site in structure], (-1, 3))
242239

243240
# Set small values to zero. This usually happens when the CIF file
244241
# does not give structure parameters with enough digits.
245-
rprim = np.where(np.abs(rprim) > 1e-8, rprim, 0.0)
246-
xred = np.where(np.abs(xred) > 1e-8, xred, 0.0)
242+
r_prim = np.where(np.abs(r_prim) > 1e-8, r_prim, 0.0)
243+
x_red = np.where(np.abs(x_red) > 1e-8, x_red, 0.0)
247244

248245
# Info on atoms.
249246
dct = {
250247
"natom": n_atoms,
251248
"ntypat": n_types_atom,
252249
"typat": typat,
253250
"znucl": znucl_type,
254-
"xred": xred,
251+
"xred": x_red,
252+
"properties": structure.properties,
255253
}
256254

257255
# Add info on the lattice.
258256
# Should we use (rprim, acell) or (angdeg, acell) to specify the lattice?
259-
geomode = kwargs.pop("geomode", "rprim")
260-
if geomode == "automatic":
261-
geomode = "rprim"
257+
geo_mode = kwargs.pop("geomode", "rprim")
258+
if geo_mode == "automatic":
259+
geo_mode = "rprim"
262260
if structure.lattice.is_hexagonal: # or structure.lattice.is_rhombohedral
263-
geomode = "angdeg"
264-
angdeg = structure.lattice.angles
261+
geo_mode = "angdeg"
262+
ang_deg = structure.lattice.angles
265263
# Here one could polish a bit the numerical values if they are not exact.
266264
# Note that in pmg the angles are 12, 20, 01 while in Abinit 12, 02, 01
267265
# One should make sure that the orientation is preserved (see Curtarolo's settings)
268266

269-
if geomode == "rprim":
267+
if geo_mode == "rprim":
270268
dct.update(
271269
acell=3 * [1.0],
272-
rprim=rprim,
270+
rprim=r_prim,
273271
)
274272

275-
elif geomode == "angdeg":
273+
elif geo_mode == "angdeg":
276274
dct.update(
277275
acell=ArrayWithUnit(structure.lattice.abc, "ang").to("bohr"),
278-
angdeg=angdeg,
276+
angdeg=ang_deg,
279277
)
280278
else:
281-
raise ValueError(f"Wrong value for {geomode=}")
279+
raise ValueError(f"Wrong value for {geo_mode=}")
282280

283281
return dct
284282

pymatgen/io/abinit/variable.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import collections
66
import collections.abc
77
import string
8-
from collections.abc import Iterable
8+
from collections.abc import Sequence
99

1010
import numpy as np
1111

@@ -23,7 +23,7 @@
2323
class InputVariable:
2424
"""An Abinit input variable."""
2525

26-
def __init__(self, name, value, units="", valperline=3):
26+
def __init__(self, name: str, value, units: str = "", valperline: int = 3) -> None:
2727
"""
2828
Args:
2929
name: Name of the variable.
@@ -35,12 +35,11 @@ def __init__(self, name, value, units="", valperline=3):
3535
self.value = value
3636
self._units = units
3737

38-
# Maximum number of values per line.
39-
self.valperline = valperline
38+
self.valperline = valperline # Maximum number of values per line.
4039
if name == "bdgw":
4140
self.valperline = 2
4241

43-
if isinstance(self.value, Iterable) and isinstance(self.value[-1], str) and self.value[-1] in _UNITS:
42+
if isinstance(self.value, Sequence) and isinstance(self.value[-1], str) and self.value[-1] in _UNITS:
4443
self.value = list(self.value)
4544
self._units = self.value.pop(-1)
4645

tests/core/test_structure.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -203,37 +203,34 @@ def test_as_dict(self):
203203
struct = IStructure(self.lattice, [{si: 0.5, mn: 0.5}, {si: 0.5}], coords)
204204
assert "lattice" in struct.as_dict()
205205
assert "sites" in struct.as_dict()
206-
d = self.propertied_structure.as_dict()
207-
assert d["sites"][0]["properties"]["magmom"] == 5
206+
dct = self.propertied_structure.as_dict()
207+
assert dct["sites"][0]["properties"]["magmom"] == 5
208208
coords = [[0, 0, 0], [0.75, 0.5, 0.75]]
209209
struct = IStructure(
210210
self.lattice,
211-
[
212-
{Species("O", -2, spin=3): 1.0},
213-
{Species("Mg", 2, spin=2): 0.8},
214-
],
211+
[{Species("O", -2, spin=3): 1.0}, {Species("Mg", 2, spin=2): 0.8}],
215212
coords,
216213
site_properties={"magmom": [5, -5]},
217214
properties={"general_property": "test"},
218215
)
219-
d = struct.as_dict()
220-
assert d["sites"][0]["properties"]["magmom"] == 5
221-
assert d["sites"][0]["species"][0]["spin"] == 3
222-
assert d["properties"]["general_property"] == "test"
216+
dct = struct.as_dict()
217+
assert dct["sites"][0]["properties"]["magmom"] == 5
218+
assert dct["sites"][0]["species"][0]["spin"] == 3
219+
assert dct["properties"]["general_property"] == "test"
223220

224-
d = struct.as_dict(0)
225-
assert "volume" not in d["lattice"]
226-
assert "xyz" not in d["sites"][0]
221+
dct = struct.as_dict(0)
222+
assert "volume" not in dct["lattice"]
223+
assert "xyz" not in dct["sites"][0]
227224

228225
def test_from_dict(self):
229-
d = self.propertied_structure.as_dict()
230-
struct = IStructure.from_dict(d)
226+
dct = self.propertied_structure.as_dict()
227+
struct = IStructure.from_dict(dct)
231228
assert struct[0].magmom == 5
232-
d = self.propertied_structure.as_dict(0)
233-
s2 = IStructure.from_dict(d)
229+
dct = self.propertied_structure.as_dict(0)
230+
s2 = IStructure.from_dict(dct)
234231
assert struct == s2
235232

236-
d = {
233+
dct = {
237234
"lattice": {
238235
"a": 3.8401979337,
239236
"volume": 40.044794644251596,
@@ -282,7 +279,7 @@ def test_from_dict(self):
282279
],
283280
"properties": {"test_property": "test"},
284281
}
285-
struct = IStructure.from_dict(d)
282+
struct = IStructure.from_dict(dct)
286283
assert struct[0].magmom == 5
287284
assert struct[0].specie.spin == 3
288285
assert struct.properties["test_property"] == "test"
@@ -818,7 +815,7 @@ def test_get_dist_matrix(self):
818815
ans = [[0.0, 2.3516318], [2.3516318, 0.0]]
819816
assert_allclose(self.struct.distance_matrix, ans)
820817

821-
def test_to_from_file_string(self):
818+
def test_to_from_file_and_string(self):
822819
for fmt in ["cif", "json", "poscar", "cssr"]:
823820
struct = self.struct.to(fmt=fmt)
824821
assert struct is not None
@@ -855,8 +852,8 @@ def test_to_from_file_string(self):
855852
with pytest.raises(ValueError, match="Invalid format='badformat'"):
856853
self.struct.to(fmt="badformat")
857854

858-
self.struct.to(filename="POSCAR.testing.gz")
859-
struct = Structure.from_file("POSCAR.testing.gz")
855+
self.struct.to(filename=(gz_json_path := "POSCAR.testing.gz"))
856+
struct = Structure.from_file(gz_json_path)
860857
assert struct == self.struct
861858

862859
# test CIF file with unicode error
@@ -897,6 +894,7 @@ def setUp(self):
897894
coords = [[0, 0, 0], [0.75, 0.5, 0.75]]
898895
lattice = Lattice([[3.8401979337, 0, 0], [1.9200989668, 3.3257101909, 0], [0, -2.2171384943, 3.1355090603]])
899896
self.struct = Structure(lattice, ["Si", "Si"], coords)
897+
self.struct.properties["foo"] = "bar"
900898
self.cu_structure = Structure(lattice, ["Cu", "Cu"], coords)
901899
self.disordered = Structure.from_spacegroup("Im-3m", Lattice.cubic(3), [Composition("Fe0.5Mn0.5")], [[0, 0, 0]])
902900
self.labeled_structure = Structure(lattice, ["Si", "Si"], coords, labels=["Si1", "Si2"])
@@ -1271,13 +1269,14 @@ def test_as_from_dict(self):
12711269
assert isinstance(s1, Structure)
12721270

12731271
def test_default_dict_attrs(self):
1274-
d = self.struct.as_dict()
1275-
assert d["charge"] == 0
1272+
dct = self.struct.as_dict()
1273+
assert dct["charge"] == 0
1274+
assert dct["properties"] == {"foo": "bar"}
12761275

12771276
def test_to_from_abivars(self):
12781277
"""Test as_dict, from_dict with fmt == abivars."""
1279-
d = self.struct.as_dict(fmt="abivars")
1280-
s2 = Structure.from_dict(d, fmt="abivars")
1278+
dct = self.struct.as_dict(fmt="abivars")
1279+
s2 = Structure.from_dict(dct, fmt="abivars")
12811280
assert s2 == self.struct
12821281
assert isinstance(s2, Structure)
12831282

@@ -1292,8 +1291,8 @@ def test_to_from_file_string(self):
12921291
assert isinstance(ss, Structure)
12931292

12941293
# to/from file
1295-
self.struct.to(filename="POSCAR.testing")
1296-
assert os.path.isfile("POSCAR.testing")
1294+
self.struct.to(filename=(poscar_path := "POSCAR.testing"))
1295+
assert os.path.isfile(poscar_path)
12971296

12981297
for ext in (".json", ".json.gz", ".json.bz2", ".json.xz", ".json.lzma"):
12991298
self.struct.to(filename=f"json-struct{ext}")

tests/io/abinit/test_inputs.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ def test_api(self):
256256

257257
class TestShiftMode(PymatgenTest):
258258
def test_shiftmode(self):
259-
"""Testing shiftmode."""
260259
gamma = ShiftMode.GammaCentered
261260
assert ShiftMode.from_object("G") == gamma
262261
assert ShiftMode.from_object(gamma) == gamma

0 commit comments

Comments
 (0)