Skip to content

Commit 2701418

Browse files
authored
Merge pull request #73 from mikibonacci/roundtrips
This fixes #71
2 parents cdea4b6 + e3d9774 commit 2701418

File tree

11 files changed

+196
-105
lines changed

11 files changed

+196
-105
lines changed

docs/source/how_to/creation_mutation.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,19 @@ Magnetic moments:
270270
[[0. 0. 2.5]]
271271
```
272272

273-
### From Legacy AiiDA StructureData
273+
### Backward compatibility: from Legacy orm.StructureData
274274

275-
Migrate from the legacy AiiDA StructureData:
275+
It is possible to migrate from the old `orm.StructureData` to the new `aiida-atomistic` `StructureData`:
276276

277277
```python
278-
from aiida import orm
279-
from aiida_atomistic.data.structure import StructureData
278+
from aiida.orm import StructureData as LegacyStructureData
279+
from aiida_atomistic.data.structure.utils_orm import from_legacy_to_atomistic
280280

281-
# Load legacy structure
282-
legacy_structure = orm.StructureData()
283-
legacy_structure.append_atom(symbols='C', position=(0.0, 0.0, 0.0))
281+
legacy = LegacyStructureData(cell=[[3.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 3.0]])
282+
legacy.append_atom(symbols='H', position=[0.0, 0.0, 0.0], mass=1.008, name='H1')
283+
legacy.append_atom(symbols='O', position=[0.0, 0.0, 1.0], mass=15.999, name='O1')
284284

285-
# Convert to new format
286-
new_structure = legacy_structure.to_atomistic()
285+
s = from_legacy_to_atomistic(legacy_structure=legacy, metadata={'store_provenance': True}) # no need to define metadata, default is already True.
287286
```
288287

289288
## Modifying Structures
@@ -292,7 +291,7 @@ new_structure = legacy_structure.to_atomistic()
292291
`StructureData` is **immutable** and cannot be modified. Use `StructureBuilder` for modifications.
293292
:::
294293

295-
### Creating a Mutable Structure
294+
### Creating a Mutable Structure: the `StructureBuilder`
296295

297296
```python
298297
from aiida_atomistic.data.structure import StructureBuilder
@@ -436,17 +435,20 @@ print(f"Magmoms after removal: {mutable.properties.magmoms}") # None
436435
- See the full list of properties in the [Site API documentation](../reference/api/auto/aiida_atomistic/data/structure/site/index.rst)
437436
:::
438437

439-
### Converting Between Mutable and Immutable
438+
## Conversion Between `StructureBuilder` and `StructureData` (and viceversa)
439+
440+
The `StructureBuilder` object is a pure python class, any instance of it needs to be converted into the `StructureData` before being used in an AiiDA process.
441+
It is possible to seamlessly convert between `StructureBuilder` and `StructureData` (and viceversa) using the defined `to_*` and `from_*` methods:
440442

441443
```python
442-
# Mutable → Immutable (for storage in AiiDA)
443-
immutable = StructureData(**mutable.to_dict())
444+
# StructureBuilder → StructureData (for storage in AiiDA)
445+
structuredata = structurebuilder.to_aiida()
446+
structuredata = StructureData.from_builder(structurebuilder)
444447

445448
# Immutable → Mutable (for editing)
446-
mutable_copy = StructureBuilder(**immutable.to_dict())
449+
structurebuilder = structuredata.to_builder()
450+
structurebuilder = StructureBuilder.from_aiida(structuredata)
447451

448-
# Or use get_value()
449-
mutable_copy2 = immutable.get_value()
450452
```
451453

452454
## Generating Kinds Automatically

docs/source/how_to/kinds.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ By default, `StructureData` validates kinds on creation:
171171

172172
```python
173173
# This validates that sites with the same kind_name have identical properties
174-
structure = StructureData(**structure_dict, validate_kinds=True) # Default
174+
structure = StructureData(**structure_dict, validate_kinds=True) # Default is False
175175
```
176176

177177
If validation fails, you'll get an error:
@@ -211,7 +211,7 @@ In some cases, you may want to skip validation:
211211

212212
```python
213213
# Skip validation on creation
214-
structure = StructureData(**structure_dict, validate_kinds=False)
214+
structure = StructureData(**structure_dict, validate_kinds=False) # Default behavior
215215
```
216216

217217
:::{warning}

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ dependencies = [
2626
"voluptuous",
2727
"ase",
2828
"pymatgen>=2022.1.20",
29-
"pydantic>=2.6"
29+
"pydantic>=2.6",
30+
"scipy>=1.10",
3031
]
3132

3233
[project.urls]

src/aiida_atomistic/data/structure/getter_mixin.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
HubbardGetterMixin,
1414
)
1515

16-
from aiida_atomistic.data.structure.utils import classify_site_kinds, check_kinds_match, efficient_copy
16+
from aiida_atomistic.data.structure.utils import classify_site_kinds, check_kinds_match
1717

1818
try:
1919
import ase # noqa: F401
@@ -388,16 +388,15 @@ def validate_kinds(self,):
388388
raise ValueError("The kinds defined in the structure do not match the generated kinds from the sites. Please run the 'generate_kinds' method to see the expected kinds.")
389389

390390
# TO methods:
391-
def to_dict(self, exclude_kinds=False):
391+
def to_dict(self):
392392
"""
393393
Convert the structure to a dictionary representation.
394394
395-
:param detect_kinds: Whether to detect and include the kinds of the structure.
396-
:type detect_kinds: bool, optional
397395
:return: The structure as a dictionary.
398396
:rtype: dict
399397
"""
400-
dict_repr = efficient_copy(self.properties.model_dump(exclude_unset=True, exclude_none=True, warnings=False, exclude={'kinds'} if exclude_kinds else {}))
398+
exclude = set(self.properties.model_computed_fields.keys())
399+
dict_repr = copy.deepcopy(self.properties.model_dump(exclude_unset=True, exclude_none=True, warnings=False, exclude=exclude))
401400

402401
return dict_repr
403402

src/aiida_atomistic/data/structure/models.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class StructureBaseModel(BaseModel):
6464
from_attributes=True,
6565
frozen=False,
6666
arbitrary_types_allowed=True,
67+
extra='forbid',
6768
#validate_assignment=True
6869
)
6970

@@ -116,9 +117,9 @@ def validate_sites(cls, v):
116117

117118
if v is None:
118119
return v
119-
else:
120-
# test if they can be converted to Site
121-
sites = [Site.model_validate(site) if not isinstance(site, Site) else site for site in v]
120+
# else:
121+
# # test if they can be converted to Site
122+
# sites = [Site.model_validate(site) if not isinstance(site, Site) else site for site in v]
122123

123124
_check_valid_sites(v)
124125

@@ -291,20 +292,34 @@ def kinds(self) -> list[Kind]:
291292
#raise ValueError("Kind names must be defined to access kinds.")
292293
return None
293294

295+
# Mapping of kind_name -> site indices
296+
kind_to_indices = defaultdict(list)
297+
for i, name in enumerate(self.kind_names):
298+
kind_to_indices[name].append(i)
299+
300+
positions_array = self.positions
301+
294302
kinds_list = []
295-
kind_name_set = set(self.kind_names)
296-
for idx, site in enumerate(self.sites):
303+
seen_kinds = set()
304+
305+
for site in self.sites:
297306
kind_name = site.kind_name if site.kind_name else site.symbol
298-
if kind_name in kind_name_set:
299-
site_indices = [i for i, name in enumerate(self.kind_names) if name == kind_name]
300-
positions=np.array([self.positions[i] for i in site_indices])
301-
kind = Kind(
302-
**site.model_dump(exclude={'position'}),
303-
site_indices=site_indices,
304-
positions=positions,
305-
)
306-
kinds_list.append(kind)
307-
kind_name_set.remove(kind_name) # Ensure we don't add the same kind multiple
307+
308+
# Skip if we've already processed this kind
309+
if kind_name in seen_kinds:
310+
continue
311+
312+
seen_kinds.add(kind_name)
313+
site_indices = kind_to_indices[kind_name]
314+
positions = positions_array[site_indices]
315+
316+
kind = Kind(
317+
**site.model_dump(exclude={'position','kind_name'}),
318+
site_indices=site_indices,
319+
positions=positions,
320+
kind_name=kind_name
321+
)
322+
kinds_list.append(kind)
308323

309324
return FrozenList(kinds_list)
310325

src/aiida_atomistic/data/structure/site.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,9 @@ def check_minimal_requirements(cls, data):
168168
#if "kind_name" not in data:
169169
# data["kind_name"] = data["symbol"]
170170

171-
for prop in data.keys():
172-
if cls._mutable:
171+
172+
if cls._mutable:
173+
for prop in data.keys():
173174
data[prop] = freeze_nested(data[prop])
174175

175176
return data

src/aiida_atomistic/data/structure/structure.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class StructureData(Data, GetterMixin):
1818
_mutable = False
1919
_model = ImmutableStructureModel
2020

21-
def __init__(self, validate_kinds=True, sites:list[dict]=None, kinds:list[dict]=None, **kwargs):
21+
def __init__(self, sites:list[dict]=None, kinds:list[dict]=None, **kwargs):
2222

2323
if sites is not None and kinds is not None:
2424
warnings.warn("Provided both `sites` and `kinds` information. Dropping the `sites` information and using only `kinds`.")
@@ -29,8 +29,8 @@ def __init__(self, validate_kinds=True, sites:list[dict]=None, kinds:list[dict]=
2929
self._properties = self._model(sites=sites, **kwargs)
3030
super().__init__()
3131

32-
if validate_kinds and self.kinds is not None:
33-
self.validate_kinds()
32+
#if validate_kinds:
33+
# if self.kinds is not None: self.validate_kinds()
3434

3535
attributes = self.properties.model_dump(exclude_unset=True, exclude_none=True, warnings=False)
3636
if self.properties.kind_names is not None:
@@ -58,15 +58,13 @@ def properties(self):
5858
return self._properties
5959

6060
@classmethod
61-
def from_builder(cls, mutable_structure, validate_kinds=True):
62-
if not isinstance(mutable_structure, StructureBuilder):
63-
raise ValueError(f"Input structure should be of type StructureBuilder, not {type(mutable_structure)}")
64-
return cls(validate_kinds=validate_kinds, **mutable_structure.to_dict(exclude_kinds=True))
61+
def from_builder(cls, builder: 'StructureBuilder'):
62+
from aiida_atomistic.data.structure.structure import StructureBuilder
63+
if not isinstance(builder, StructureBuilder):
64+
raise ValueError(f"Input builder should be of type StructureBuilder, not {type(builder)}")
65+
return cls(**builder.to_dict())
6566

66-
def to_mutable(self,):
67-
return StructureBuilder(**self.to_dict())
68-
69-
def get_value(self):
67+
def to_builder(self) -> 'StructureBuilder':
7068
return StructureBuilder(**self.to_dict())
7169

7270
def __repr__(self) -> str:
@@ -89,7 +87,7 @@ class StructureBuilder(GetterMixin, SetterMixin):
8987
_mutable = True
9088
_model = MutableStructureModel
9189

92-
def __init__(self, validate_kinds=True, sites:list[dict]=None, kinds:list[dict]=None, **kwargs):
90+
def __init__(self, sites:list[dict]=None, kinds:list[dict]=None, **kwargs):
9391

9492
if sites is not None and kinds is not None:
9593
warnings.warn("Provided both `sites` and `kinds` information. Dropping the `sites` information and using only `kinds`.")
@@ -104,6 +102,15 @@ def __init__(self, validate_kinds=True, sites:list[dict]=None, kinds:list[dict]=
104102
def properties(self):
105103
return self._properties
106104

105+
@classmethod
106+
def from_aiida(cls, aiida: 'StructureData'):
107+
if not isinstance(aiida, StructureBuilder):
108+
raise ValueError(f"Input aiida should be of type StructureBuilder, not {type(aiida)}")
109+
return cls(**aiida.to_dict())
110+
111+
def to_aiida(self) -> 'StructureData':
112+
return StructureData(**self.to_dict())
113+
107114
def __repr__(self) -> str:
108115
"""Return a concise string representation of the structure."""
109116
prop_repr_str = self.properties.__repr__()

0 commit comments

Comments
 (0)