From structures to a phase diagram

This example follows a small offline data-preparation flow: httk-atomistic builds structures, httk-store stores and searches both structure records and composition–energy inputs, and httk-analyse constructs a phase diagram from the queried inputs. The objects stay in memory until the explicit SQLite write, so the example is deterministic and needs no input files or network access.

import math
import tempfile
from pathlib import Path

from httk.analyse.matsci import PhaseDiagram
from httk.atomistic import (
    Cell,
    Sites,
    Species,
    StructureEntry,
    UnitcellStructure,
    UnitcellStructureRecord,
)
from httk.core import DataRecord
from httk.core.storage import content_id
from httk.store import Backend, EntryIdScheme, SqlStore

tmpdir = Path(tempfile.mkdtemp(prefix='httk-store-analysis-'))
species = [
    Species('Na', ('Na',), (1.0,)),
    Species('Cl', ('Cl',), (1.0,)),
]
base = UnitcellStructure(
    Cell([[3, 0, 0], [0, 3, 0], [0, 0, 3]]),
    Sites([[0, 0, 0], [0.5, 0.5, 0.5]]),
    species,
    ['Na', 'Cl'],
    optimization_type='local',
)
supercell = base.supercell([[2, 0, 0], [0, 1, 0], [0, 0, 1]]).structure
sodium = UnitcellStructure(
    base.cell,
    Sites([[0, 0, 0]]),
    [species[0]],
    ['Na'],
)
assert len(base.sites) == 2
assert len(supercell.sites) == 4
structures = [base, supercell, sodium]
phase_inputs = [
    DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'Na', 'composition': {'Na': 1}, 'energy': 0.0}),
    DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'Cl', 'composition': {'Cl': 1}, 'energy': 0.0}),
    DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'NaCl', 'composition': {'Na': 1, 'Cl': 1}, 'energy': -2.0}),
    DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'NaCl3', 'composition': {'Na': 1, 'Cl': 3}, 'energy': -1.0}),
]
assert len(phase_inputs) == 4
database = Backend.sqlite(tmpdir / 'structures.sqlite')
store = SqlStore(
    database,
    entry_records={StructureEntry: (UnitcellStructureRecord,)},
    entry_ids=EntryIdScheme('httk.notebook', '1'),
)
for structure in structures:
    store.save(structure)
for phase_input in phase_inputs:
    store.save(phase_input)

# The store mints a public entry id (shared by every revision of a lineage) and
# a per-revision immutable id of the form <id>~<n>. content_id stays the storage
# identity used to fetch a record back; the ids are not set by the caller.
fetched = store.fetch_entry(StructureEntry, content_id(base), eager=True)
assert isinstance(fetched, UnitcellStructureRecord)
assert fetched.id.startswith('httk.notebook-1-')
assert fetched.immutable_id == f'{fetched.id}~1'

search = store.searcher()
record = search.variable(UnitcellStructureRecord)
search.add(record.immutable_id == fetched.immutable_id)
match = search.results(record=record).one().record
assert match.immutable_id == fetched.immutable_id
print('content id:', content_id(base))
print('minted entry id:', fetched.id)
print('first revision immutable id:', fetched.immutable_id)
print('search match:', match.immutable_id)

phase_search = store.searcher()
phase_record = phase_search.variable(DataRecord)
phase_search.add(phase_record.name == 'phase-input')
queried_rows = sorted(
    (row.record.value for row in phase_search.results(record=phase_record)),
    key=lambda row: row['id'],
)
assert [row['id'] for row in queried_rows] == ['Cl', 'Na', 'NaCl', 'NaCl3']
assert [row['energy'] for row in queried_rows] == [0.0, 0.0, -2.0, -1.0]
content id: a4aceaff3625e46f7f5a24b1fedd2b708a994e111d0c8e006c1eb3d405a018c5
minted entry id: httk.notebook-1-1
first revision immutable id: httk.notebook-1-1~1
search match: httk.notebook-1-1~1
# These queried synthetic energies keep the analysis example small; they are not material data.
diagram = PhaseDiagram.from_compositions(
    [row['composition'] for row in queried_rows],
    [row['energy'] for row in queried_rows],
    [row['id'] for row in queried_rows],
)
assert diagram.hull_indices == (0, 1, 2)
assert math.isclose(diagram.energy_above_hull[3], 0.25, rel_tol=0, abs_tol=1e-9)

hull_rows = list(zip(diagram.ids, diagram.energy_above_hull))
for identifier, excess in hull_rows:
    print(f'{identifier:5s}  energy above hull = {excess:.2f} eV/atom')
Cl     energy above hull = 0.00 eV/atom
Na     energy above hull = 0.00 eV/atom
NaCl   energy above hull = 0.00 eV/atom
NaCl3  energy above hull = 0.25 eV/atom

A file-backed store can be exposed through httk-serve’s OPTIMADE interface, while the storage declaration and query DSL are documented in the httk-store database guide. For larger datasets, see the httk-analyse phase-diagram API and its incremental builder.