{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# From structures to a phase diagram\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import math\n", "import tempfile\n", "from pathlib import Path\n", "\n", "from httk.analyse.matsci import PhaseDiagram\n", "from httk.atomistic import (\n", " Cell,\n", " Sites,\n", " Species,\n", " StructureEntry,\n", " UnitcellStructure,\n", " UnitcellStructureRecord,\n", ")\n", "from httk.core import DataRecord\n", "from httk.core.storage import content_id\n", "from httk.store import Backend, EntryIdScheme, SqlStore\n", "\n", "tmpdir = Path(tempfile.mkdtemp(prefix='httk-store-analysis-'))\n", "species = [\n", " Species('Na', ('Na',), (1.0,)),\n", " Species('Cl', ('Cl',), (1.0,)),\n", "]\n", "base = UnitcellStructure(\n", " Cell([[3, 0, 0], [0, 3, 0], [0, 0, 3]]),\n", " Sites([[0, 0, 0], [0.5, 0.5, 0.5]]),\n", " species,\n", " ['Na', 'Cl'],\n", " optimization_type='local',\n", ")\n", "supercell = base.supercell([[2, 0, 0], [0, 1, 0], [0, 0, 1]]).structure\n", "sodium = UnitcellStructure(\n", " base.cell,\n", " Sites([[0, 0, 0]]),\n", " [species[0]],\n", " ['Na'],\n", ")\n", "assert len(base.sites) == 2\n", "assert len(supercell.sites) == 4\n", "structures = [base, supercell, sodium]\n", "phase_inputs = [\n", " DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'Na', 'composition': {'Na': 1}, 'energy': 0.0}),\n", " DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'Cl', 'composition': {'Cl': 1}, 'energy': 0.0}),\n", " DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'NaCl', 'composition': {'Na': 1, 'Cl': 1}, 'energy': -2.0}),\n", " DataRecord.from_value('urn:notebook:phase-input', 'phase-input', {'id': 'NaCl3', 'composition': {'Na': 1, 'Cl': 3}, 'energy': -1.0}),\n", "]\n", "assert len(phase_inputs) == 4" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "database = Backend.sqlite(tmpdir / 'structures.sqlite')\n", "store = SqlStore(\n", " database,\n", " entry_records={StructureEntry: (UnitcellStructureRecord,)},\n", " entry_ids=EntryIdScheme('httk.notebook', '1'),\n", ")\n", "for structure in structures:\n", " store.save(structure)\n", "for phase_input in phase_inputs:\n", " store.save(phase_input)\n", "\n", "# The store mints a public entry id (shared by every revision of a lineage) and\n", "# a per-revision immutable id of the form ~. content_id stays the storage\n", "# identity used to fetch a record back; the ids are not set by the caller.\n", "fetched = store.fetch_entry(StructureEntry, content_id(base), eager=True)\n", "assert isinstance(fetched, UnitcellStructureRecord)\n", "assert fetched.id.startswith('httk.notebook-1-')\n", "assert fetched.immutable_id == f'{fetched.id}~1'\n", "\n", "search = store.searcher()\n", "record = search.variable(UnitcellStructureRecord)\n", "search.add(record.immutable_id == fetched.immutable_id)\n", "match = search.results(record=record).one().record\n", "assert match.immutable_id == fetched.immutable_id\n", "print('content id:', content_id(base))\n", "print('minted entry id:', fetched.id)\n", "print('first revision immutable id:', fetched.immutable_id)\n", "print('search match:', match.immutable_id)\n", "\n", "phase_search = store.searcher()\n", "phase_record = phase_search.variable(DataRecord)\n", "phase_search.add(phase_record.name == 'phase-input')\n", "queried_rows = sorted(\n", " (row.record.value for row in phase_search.results(record=phase_record)),\n", " key=lambda row: row['id'],\n", ")\n", "assert [row['id'] for row in queried_rows] == ['Cl', 'Na', 'NaCl', 'NaCl3']\n", "assert [row['energy'] for row in queried_rows] == [0.0, 0.0, -2.0, -1.0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# These queried synthetic energies keep the analysis example small; they are not material data.\n", "diagram = PhaseDiagram.from_compositions(\n", " [row['composition'] for row in queried_rows],\n", " [row['energy'] for row in queried_rows],\n", " [row['id'] for row in queried_rows],\n", ")\n", "assert diagram.hull_indices == (0, 1, 2)\n", "assert math.isclose(diagram.energy_above_hull[3], 0.25, rel_tol=0, abs_tol=1e-9)\n", "\n", "hull_rows = list(zip(diagram.ids, diagram.energy_above_hull))\n", "for identifier, excess in hull_rows:\n", " print(f'{identifier:5s} energy above hull = {excess:.2f} eV/atom')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A file-backed store can be exposed through [httk-serve's OPTIMADE interface](https://docs.httk.org/httk-serve/dev/main/optimade/), while the storage declaration and query DSL are documented in the [httk-store database guide](https://docs.httk.org/httk-store/dev/main/details/db/). For larger datasets, see the [httk-analyse phase-diagram API](https://docs.httk.org/httk-analyse/dev/main/phase-diagrams/) and its incremental builder." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }