Serving structures as OPTIMADE, with derived fields and custom properties¶
StructureEntryProvider is the adapter that turns a plain {id: Structure}
mapping into something an OPTIMADE server can query. It implements the neutral
httk.core.EntryProvider contract — entry_types(), property_keys(),
records() — and nothing about it is specific to any particular server. That is
the point of the seam: httk-atomistic has the data, httk-optimade serves it,
and neither imports the other. They meet at the contract.
What the provider derives for you. Beyond the structural fields it reads
straight off the Structure (lattice_vectors, cartesian_site_positions,
species, species_at_sites, nsites, elements, nelements), it computes
the standard OPTIMADE composition fields:
chemical_formula_reduced— element counts divided by their gcd, elements in alphabetical order. Four Fe, twelve O and four Sm reduce by 4 toFeO3Sm.chemical_formula_anonymous— the same stoichiometry with the elements replaced byA,B,C, … assigned by descending amount, so the same formula givesA3BC.elements_ratios— each element’s fraction of the sites, ordered to matchelements.nperiodic_dimensionsanddimension_types— 3 and[1, 1, 1], since aStructureis a fully periodic crystal.structure_features— the OPTIMADE flag list. A site with mixed occupancy makes this["disorder"]; a species with attached particles adds"site_attachments"; a fully ordered structure gets[].
Limits of derivation. The composition fields are only
meaningful when every species is a single, unattached element — there is no
sensible integer count of Fe in a site that is half Fe and half Ni. So for a
disordered structure the provider serves null for those derived fields rather
than inventing a number, while still serving elements, nelements, nsites,
and the honest structure_features: ["disorder"]. Compare the two entries in
the output below; this asymmetry is the interesting part.
Entries with no structure. Passing None for an id is legal and means “this
entry exists, but I have no structure for it”. Every structural and derived
field then serves null. It keeps a known id queryable instead of making it
vanish.
Custom properties. A database usually has more to say than the standard
fields — a total energy, a provenance tag. Pass a PropertyDefinition per
custom name in extra_definitions (the name needs a registered database prefix,
hence the leading _httk_), and the per-entry values in properties. The
provider validates that every value you supply is described by the extended
definition, so a typo is an error at construction time rather than a silently
missing field at query time.
The last section runs an actual query: adapter_from_providers wraps the
provider for httk-optimade’s backend, and execute_query runs a parsed
OPTIMADE filter against it. httk-optimade is an optional peer distribution,
not a dependency.
from httk.core import PropertyDefinition
# pyright: reportMissingImports=false
# httk-optimade is an optional peer distribution, so it is not guaranteed to be
# installed where this repository's type checks run; see HTTK_EXAMPLE_REQUIRES below.
from httk.optimade import adapter_from_providers, parse_optimade_filter
from httk.optimade.backend import execute_query
from httk.atomistic import Species, Structure, StructureEntryProvider
#: This example needs the optional peer distribution *httk-optimade* on the path.
HTTK_EXAMPLE_REQUIRES = ["httk.optimade"]
#: An ordered SmFeO3-like cell: 4 Fe, 12 O, 4 Sm on twenty sites.
FE = Species(name="Fe", chemical_symbols=("Fe",), concentration=(1.0,))
OXYGEN = Species(name="O", chemical_symbols=("O",), concentration=(1.0,))
SM = Species(name="Sm", chemical_symbols=("Sm",), concentration=(1.0,))
#: A single site shared 50/50 between Fe and Ni -- a substitutional alloy.
FE_NI = Species(name="M", chemical_symbols=("Fe", "Ni"), concentration=(0.5, 0.5))
def ordered_smfeo3() -> Structure:
cell = [[5.6, 0.0, 0.0], [0.0, 7.6, 0.0], [0.0, 0.0, 5.3]]
sites = [[0.01 * index, 0.0, 0.0] for index in range(20)]
return Structure(cell, sites, [FE, OXYGEN, SM], ["Fe"] * 4 + ["O"] * 12 + ["Sm"] * 4)
def disordered_alloy() -> Structure:
cell = [[3.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 3.0]]
return Structure(cell, [[0.0, 0.0, 0.0]], [FE_NI], ["M"])
def build_provider() -> StructureEntryProvider:
"""A provider over three entries: one ordered, one disordered, and one with no structure."""
total_energy = PropertyDefinition.from_simple(
"_httk_total_energy",
description="Total energy of the calculation, in eV.",
fulltype="float",
)
return StructureEntryProvider(
{
"smfeo3": ordered_smfeo3(),
"feni-alloy": disordered_alloy(),
"known-but-empty": None,
},
extra_definitions={"_httk_total_energy": total_energy},
properties={"smfeo3": {"_httk_total_energy": -12.5}},
)
DERIVED_FIELDS = (
"chemical_formula_reduced",
"chemical_formula_anonymous",
"chemical_formula_descriptive",
"elements",
"elements_ratios",
"nelements",
"nsites",
"nperiodic_dimensions",
"dimension_types",
"structure_features",
"_httk_total_energy",
)
def show_records(provider: StructureEntryProvider) -> None:
"""Print what the provider serves, entry by entry."""
print("Served property names:")
print(" ", sorted(provider.property_keys("structures")))
print(" ('id' is served from the record key", repr(provider.property_keys("structures")["id"]) + ")")
print()
records = {record["__id"]: record for record in provider.records("structures")}
for entry_id in ("smfeo3", "feni-alloy", "known-but-empty"):
record = records[entry_id]
print(f"{entry_id}:")
for field in DERIVED_FIELDS:
print(f" {field:<28} {record[field]!r}")
print()
def show_definition(provider: StructureEntryProvider) -> None:
"""The entry-type definition is the schema; it describes more than the provider serves."""
definition = provider.entry_types()["structures"]
described = definition.properties
served = set(provider.property_keys("structures"))
print("Entry-type definition:")
print(" properties described by the (extended) definition:", len(described))
print(" properties actually served: ", len(served))
print(" described but not served (a few):", sorted(set(described) - served)[:5])
print(" the custom property is described:", "_httk_total_energy" in described)
print(" ...with description:", described["_httk_total_energy"].description)
print()
def run_queries(provider: StructureEntryProvider) -> None:
"""Hand the provider to httk-optimade and run real OPTIMADE filters against it."""
adapter = adapter_from_providers([provider])
print("Queries through httk-optimade:")
print(" entry types served:", adapter.schema.all_entries)
def query(filter_string: str | None, fields: list[str]) -> list[dict[str, object]]:
filter_ast = parse_optimade_filter(filter_string) if filter_string else None
results = execute_query(adapter, ["structures"], ["id", *fields], [], 100, 0, filter_ast)
return [result.values for result in results]
everything = query(None, [])
print(" all ids:", sorted(str(row["id"]) for row in everything))
for filter_string, fields in (
('elements HAS "Sm"', ["chemical_formula_reduced", "chemical_formula_anonymous", "elements_ratios"]),
("nelements = 2", ["structure_features", "elements"]),
("nsites > 10", ["nsites", "nperiodic_dimensions"]),
):
rows = query(filter_string, fields)
print(f" {filter_string!r} ->")
for row in rows:
print(" ", {key: row[key] for key in ("id", *fields)})
def main() -> None:
provider = build_provider()
show_records(provider)
show_definition(provider)
run_queries(provider)
if __name__ == "__main__":
main()