Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5559,6 +5559,24 @@
"x-optimade-queryable": "optional",
"x-optimade-support": "optional"
},
"wyckoff_positions": {
"anyOf": [
{
"items": {
"type": "string",
"pattern": "^[a-z\u03b1]$"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Wyckoff Positions",
"description": "A list of Wyckoff symbols of sites (where values for sites are specified with the same order of the property `cartesian_site_positions` and/or `fractional_site_positions`).\n\n- **Type**: list of strings\n\n- **Requirements/Conventions**:\n - **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.\n - **Query**: Support for queries on this property is OPTIONAL.\n If supported, filters MAY support only a subset of comparison operators.\n - MUST have length equal to the number of sites in the structure (first dimension of the list properties `cartesian_site_positions` and/or `fractional_site_positions`).\n - If provided, MUST list a single letter (`a`-`z` or `\u03b1`) Wyckoff position for each site in the structure according to the International Tables for Crystallography vol. A (IUCr, 2016).\n\n- **Bibliographic References**:\n IUCr (2016). International Tables for Crystallography vol. A. Space-group Symmetry, Ed. M. I. Aroyo, 6-th edition. Chichester, John Wiley & Sons.",
"x-optimade-queryable": "optional",
"x-optimade-support": "optional"
},
"structure_features": {
"items": {
"$ref": "#/components/schemas/StructureFeatures"
Expand Down
36 changes: 35 additions & 1 deletion optimade/models/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pydantic import BaseModel, BeforeValidator, Field, field_validator, model_validator

from optimade.models.entries import EntryResource, EntryResourceAttributes
from optimade.models.types import ChemicalSymbol, SymmetryOperation
from optimade.models.types import ChemicalSymbol, SymmetryOperation, WyckoffSymbol
from optimade.models.utils import (
ANONYMOUS_ELEMENTS,
CHEMICAL_FORMULA_REGEXP,
Expand Down Expand Up @@ -1075,6 +1075,27 @@ class StructureResourceAttributes(EntryResourceAttributes):
),
] = None

wyckoff_positions: Annotated[
list[WyckoffSymbol] | None,
OptimadeField(
description="""A list of Wyckoff symbols of sites (where values for sites are specified with the same order of the property `cartesian_site_positions` and/or `fractional_site_positions`).

- **Type**: list of strings

- **Requirements/Conventions**:
- **Support**: OPTIONAL support in implementations, i.e., MAY be `null`.
- **Query**: Support for queries on this property is OPTIONAL.
If supported, filters MAY support only a subset of comparison operators.
- MUST have length equal to the number of sites in the structure (first dimension of the list properties `cartesian_site_positions` and/or `fractional_site_positions`).
- If provided, MUST list a single letter (`a`-`z` or `α`) Wyckoff position for each site in the structure according to the International Tables for Crystallography vol. A (IUCr, 2016).

- **Bibliographic References**:
IUCr (2016). International Tables for Crystallography vol. A. Space-group Symmetry, Ed. M. I. Aroyo, 6-th edition. Chichester, John Wiley & Sons.""",
support=SupportLevel.OPTIONAL,
queryable=SupportLevel.OPTIONAL,
),
] = None

structure_features: Annotated[
list[StructureFeatures],
OptimadeField(
Expand Down Expand Up @@ -1369,6 +1390,19 @@ def validate_species_at_sites(self) -> "StructureResourceAttributes":

return self

@model_validator(mode="after")
def validate_wyckoff_positions(self) -> "StructureResourceAttributes":
if self.wyckoff_positions is None:
return self

if self.nsites and len(self.wyckoff_positions) != self.nsites:
raise ValueError(
f"Number of wyckoff_positions (value: {len(self.wyckoff_positions)}) "
f"MUST equal number of sites (value: {self.nsites})"
)

return self

@field_validator("species", mode="after")
@classmethod
def validate_species(cls, value: list[Species] | None) -> list[Species] | None:
Expand Down
3 changes: 3 additions & 0 deletions optimade/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
EXTENDED_CHEMICAL_SYMBOLS_PATTERN,
SEMVER_PATTERN,
SYMMETRY_OPERATION_REGEXP,
WYCKOFF_SYMBOL_REGEXP,
)

__all__ = ("ChemicalSymbol", "SemanticVersion")
Expand All @@ -16,6 +17,8 @@

SymmetryOperation = Annotated[str, Field(pattern=SYMMETRY_OPERATION_REGEXP)]

WyckoffSymbol = Annotated[str, Field(pattern=WYCKOFF_SYMBOL_REGEXP)]

ElementSymbol = Annotated[str, Field(pattern=ELEMENT_SYMBOLS_PATTERN)]

SemanticVersion = Annotated[
Expand Down
1 change: 1 addition & 0 deletions optimade/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ def reduce_formula(formula: str) -> str:
CHEMICAL_FORMULA_REGEXP = r"(^$)|^([A-Z][a-z]?([2-9]|[1-9]\d+)?)+$"
SYMMETRY_OPERATION_REGEXP = r"^([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?),([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?),([-+]?[xyz]([-+][xyz])?([-+](1/2|[12]/3|[1-3]/4|[1-5]/6))?|[-+]?(1/2|[12]/3|[1-3]/4|[1-5]/6)([-+][xyz]([-+][xyz])?)?)$"
HM_SYMBOL_REGEXP = r"^(P|I|F|A|B|C|R)(\s+\d+|\s+[a-z]+|\s+\d+/[a-z]+|\s+\d+/\d+|\s+-\d*|\s+\d+/m|\s+[a-z]+/m)*$"
WYCKOFF_SYMBOL_REGEXP = r"^[a-zα]$"


def _generate_symmetry_operation_regex():
Expand Down
6 changes: 4 additions & 2 deletions tests/models/test_data/test_good_structures.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"group_probabilities": [0.3, 0.5, 0.2]
}
],
"structure_features": ["assemblies"]
"structure_features": ["assemblies"],
"wyckoff_positions": ["a", "a", "b"]
},
{
"task_id": "db/1234567",
Expand Down Expand Up @@ -61,7 +62,8 @@
"group_probabilities": [0.3, 0.5, 0.2]
}
],
"structure_features": ["assemblies"]
"structure_features": ["assemblies"],
"wyckoff_positions": ["a", "b", "b"]
},
{
"task_id": "db/1234567",
Expand Down
8 changes: 8 additions & 0 deletions tests/models/test_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ def test_bad_structures(
{"optimization_type": "not_sure"},
"Input should be 'experimental', 'hybrid', 'global', 'local', 'none', 'indeterminate' or 'other'",
),
(
{"wyckoff_positions": ["1a", "2b", "3c"]},
"3 validation errors for StructureResource\nattributes.wyckoff_positions.0\n String should match pattern '^[a-zα]$'",
),
(
{"wyckoff_positions": ["a", "b", "C"]},
"1 validation error for StructureResource\nattributes.wyckoff_positions.2\n String should match pattern '^[a-zα]$'",
),
)


Expand Down
2 changes: 1 addition & 1 deletion tests/server/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def test_list_properties(

results = cli.list_properties("structures")
for database in results:
assert len(results[database]) == 32, str(results[database])
assert len(results[database]) == 33, str(results[database])

results = cli.search_property("structures", "site")
for database in results:
Expand Down
Loading