diff --git a/other/materials_designer/specific_examples/Introduction.ipynb b/other/materials_designer/specific_examples/Introduction.ipynb index 71075fbc4..156267f34 100644 --- a/other/materials_designer/specific_examples/Introduction.ipynb +++ b/other/materials_designer/specific_examples/Introduction.ipynb @@ -24,7 +24,7 @@ "| `P-0D-NRB` | Nanoribbon | *To be added* | — | — |\n", "| `C-2D-HST` | Heterostack | [Si/SiO₂/HfO₂/TiN Heterostructure](heterostructure_silicon_silicon_dioxide_hafnium_dioxide_titanium_nitride.ipynb) | *To be added* | [[3]](#ref3) |\n", "| `C-2D-INT-S` | Interface Simple | *To be added* | — | — |\n", - "| `C-2D-INT-Z` | Interface ZSL | [BN/Graphene 2D–2D Interface](interface_2d_2d_boron_nitride_graphene.ipynb) | *To be added* | [[4]](#ref4) |\n", + "| `C-2D-INT-Z` | Interface ZSL | [BN/Graphene 2D–2D Interface](interface_2d_2d_boron_nitride_graphene.ipynb) | [Gr/h-BN Stacking Energy and Band Gap](interface_2d_2d_boron_nitride_graphene_SIMULATION.ipynb) | [[4]](#ref4) |\n", "| `C-2D-INT-Z` | Interface ZSL | [Graphene/SiO₂ 2D–3D Interface](interface_2d_3d_graphene_silicon_dioxide.ipynb) | *To be added* | [[5]](#ref5) |\n", "| `C-2D-INT-Z` | Interface ZSL | [Cu/Cristobalite 3D–3D Interface](interface_3d_3d_copper_cristobalite.ipynb) | *To be added* | [[6]](#ref6) |\n", "| `C-2D-INT-Z` | Interface ZSL | [Graphene/Ni Interface Film XY Position Optimization](optimization_interface_film_xy_position_graphene_nickel.ipynb) | [Gr/Ni(111) Registry and Work of Adhesion](optimization_interface_film_xy_position_graphene_nickel_SIMULATION.ipynb) | [[7]](#ref7) |\n", diff --git a/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene.ipynb b/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene.ipynb index 48b18a1a2..a20b1e6a7 100644 --- a/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene.ipynb +++ b/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene.ipynb @@ -8,18 +8,13 @@ "\n", "## Introduction\n", "\n", - "This notebook demonstrates the creation of an interface between two 2D materials, Boron Nitride (BN) and Graphene and shifting the film along the y-axis to create multiple (7) stacking configurations.\n", + "This notebook creates graphene on four layers of hexagonal boron nitride (h-BN) in a common hexagonal cell, for three registries of graphene on the top h-BN layer and a list of graphene–h-BN distances.\n", "\n", "Following the manuscript:\n", - "> **Jeil Jung, Ashley M. DaSilva, Allan H. MacDonald & Shaffique Adam**\n", - "> **Origin of the band gap in graphene on hexagonal boron nitride**\n", - "> Nature Communications volume 6, Article number: 6308 (2015)\n", - "> [DOI: 10.1038/ncomms7308](https://doi.org/10.1038/ncomms7308)\n", - "\n", - "\n", - "Relicating the materials with profile give in Figure 7. a (top row):\n", - "\n", - "\n" + "> **Gianluca Giovannetti, Petr A. Khomyakov, Geert Brocks, Paul J. Kelly, and Jeroen van den Brink**\n", + "> **Substrate-induced band gap in graphene on hexagonal boron nitride: Ab initio density functional calculations**\n", + "> Physical Review B 76, 073103 (2007)\n", + "> [DOI: 10.1103/PhysRevB.76.073103](https://doi.org/10.1103/PhysRevB.76.073103)" ] }, { @@ -38,31 +33,25 @@ "metadata": {}, "outputs": [], "source": [ - "FILM_MILLER_INDICES = (0, 0, 1)\n", - "FILM_THICKNESS = 1 # in atomic layers\n", - "FILM_TERMINATION_FORMULA = None # if None, the first termination will be used\n", - "FILM_VACUUM = 0.0 # in angstroms\n", - "\n", - "SUBSTRATE_MILLER_INDICES = (0, 0, 1)\n", - "SUBSTRATE_THICKNESS = 1 # in atomic layers\n", - "SUBSTRATE_TERMINATION_FORMULA = None # if None, the first termination will be used\n", - "SUBSTRATE_VACUUM = 0.0 # in angstroms\n", - "\n", - "INTERFACE_DISTANCE = 3.4 # Gap between substrate and film, in Angstrom\n", - "INTERFACE_VACUUM = 20.0 # Vacuum over film, in Angstrom\n", - "\n", - "# Whether to convert materials to conventional cells before creating slabs.\n", - "USE_CONVENTIONAL_CELL = True\n", - "\n", - "# Maximum area for the superlattice search algorithm (the final interface area will be smaller)\n", - "MAX_AREA = 350 # in Angstrom^2\n", - "# Additional fine-tuning parameters (increase values to get more strained matches):\n", - "MAX_AREA_TOLERANCE = 0.09 # in Angstrom^2\n", - "MAX_LENGTH_TOLERANCE = 0.05\n", - "MAX_ANGLE_TOLERANCE = 0.02\n", + "LATTICE_CONSTANT = 2.445 # Å, graphene LDA (Giovannetti et al. 2007); h-BN is compressed to it\n", + "H_BN_LAYERS = 4\n", + "H_BN_INTERLAYER_DISTANCE = 3.24 # Å, the paper's LDA value\n", + "VACUUM = 15.0 # Å above graphene\n", "\n", - "# Whether to reduce the resulting interface cell to the primitive cell after the interface creation.\n", - "REDUCE_RESULT_CELL_TO_PRIMITIVE = True" + "# Giovannetti et al. 2007 compute all three stackings at every distance 2.5–3.9 Å; the defaults run\n", + "# stacking (c) at three distances around its minimum. Uncomment entries to compute more\n", + "# (one job per stacking × distance, ~25 min each on queue D with two cores).\n", + "STACKINGS = [\n", + " \"c\",\n", + " # \"a\",\n", + " # \"b\",\n", + "]\n", + "DISTANCES = [\n", + " 3.1, 3.2, 3.3,\n", + " # 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9,\n", + "]\n", + "STACKING_SHIFTS = {\"a\": 0, \"b\": 1, \"c\": -1} # in units of a/√3 along y\n", + "REGISTRY_TOLERANCE = 1e-3 # crystal units" ] }, { @@ -89,7 +78,7 @@ "metadata": {}, "source": [ "### 1.3. Get input materials and assign `substrate` and `film`\n", - "Materials are loaded with `get_data()`. The first material is assigned as substrate and the second as film." + "Bulk h-BN (substrate) and graphene (film) are loaded from Standata." ] }, { @@ -101,8 +90,8 @@ "from mat3ra.standata.materials import Materials\n", "from mat3ra.made.material import Material\n", "\n", - "film = Material.create(Materials.get_by_name_first_match(\"Graphene\"))\n", - "substrate = Material.create(Materials.get_by_name_and_categories(\"BN\", \"2D\"))" + "film = Material.create(Materials.get_by_name_first_match(\"C, Graphene, HEX (P6/mmm) 2D (Monolayer), 2dm-3993\"))\n", + "substrate = Material.create(Materials.get_by_name_first_match(\"BN, Boron Nitride, HEX (P6_3/mmc) 3D (Bulk), mp-7991\"))" ] }, { @@ -127,9 +116,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Configure slabs for interface\n", + "## 2. Prepare the slabs\n", "\n", - "### 2.1. Get possible terminations for the slabs" + "### 2.1. Strain the materials to the common lattice constant\n", + "Both materials are strained in-plane to `LATTICE_CONSTANT`, and h-BN along c to `H_BN_INTERLAYER_DISTANCE` between layers." ] }, { @@ -138,51 +128,23 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.made.tools.helpers import get_slab_terminations\n", + "from mat3ra.made.tools.build_components.operations.core.modifications.strain.helpers import create_strain\n", "\n", - "film_slab_terminations = get_slab_terminations(material=film, miller_indices=FILM_MILLER_INDICES)\n", - "substrate_slab_terminations = get_slab_terminations(material=substrate, miller_indices=SUBSTRATE_MILLER_INDICES)\n", - "print(\"Film slab terminations:\", film_slab_terminations)\n", - "print(\"Substrate slab terminations:\", substrate_slab_terminations)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.2. Visualize slabs for all possible terminations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.made.tools.helpers import create_slab\n", - "\n", - "film_slabs = [create_slab(film, miller_indices=FILM_MILLER_INDICES, termination_top=termination, vacuum=0) for termination\n", - " in\n", - " film_slab_terminations]\n", - "\n", - "substrate_slabs = [create_slab(substrate, miller_indices=SUBSTRATE_MILLER_INDICES, termination_top=termination, vacuum=0)\n", - " for termination in\n", - " substrate_slab_terminations]\n", - "\n", - "film_slabs_with_titles = [{\"material\": slab, \"title\": str(termination)} for slab, termination in\n", - " zip(film_slabs, film_slab_terminations)]\n", - "substrate_slabs_with_titles = [{\"material\": slab, \"title\": str(termination)} for slab, termination in\n", - " zip(substrate_slabs, substrate_slab_terminations)]\n", - "\n", - "visualize(film_slabs_with_titles, repetitions=[3, 3, 1], rotation=\"-90x\")\n", - "visualize(substrate_slabs_with_titles, repetitions=[3, 3, 1], rotation=\"-90x\")" + "film_scale = LATTICE_CONSTANT / film.lattice.a\n", + "substrate_scale = LATTICE_CONSTANT / substrate.lattice.a\n", + "substrate_c_scale = 2 * H_BN_INTERLAYER_DISTANCE / substrate.lattice.c\n", + "film = create_strain(film, strain_matrix=[[film_scale, 0, 0], [0, film_scale, 0], [0, 0, 1]])\n", + "substrate = create_strain(\n", + " substrate, strain_matrix=[[substrate_scale, 0, 0], [0, substrate_scale, 0], [0, 0, substrate_c_scale]]\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.3. Select terminations for the Slabs" + "### 2.2. Set the AA' stacking of h-BN\n", + "The atoms of the upper layer of the bulk h-BN cell are moved by (1/3, 2/3, 0) in crystal coordinates, so that B sits over N in adjacent layers." ] }, { @@ -191,19 +153,21 @@ "metadata": {}, "outputs": [], "source": [ - "from mat3ra.made.tools.helpers import select_slab_termination\n", + "from mat3ra.made.tools.analyze.other import get_atom_indices_with_condition_on_coordinates\n", + "from mat3ra.made.tools.operations.core.unary import translate_atoms\n", "\n", - "film_termination = select_slab_termination(film_slab_terminations, FILM_TERMINATION_FORMULA)\n", - "substrate_termination = select_slab_termination(substrate_slab_terminations, SUBSTRATE_TERMINATION_FORMULA)" + "upper_layer_ids = get_atom_indices_with_condition_on_coordinates(substrate, lambda coordinate: coordinate[2] > 0.5)\n", + "substrate = translate_atoms(\n", + " substrate, atom_ids=upper_layer_ids, vector=[1 / 3, 2 / 3, 0], use_cartesian_coordinates=False\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.4. Create Substrate and Film Slabs\n", - "Slab Configuration lets define the slab thickness, vacuum, and the Miller indices of the interfacial plane and get the slabs with possible terminations.\n", - "Define the substrate slab cell that will be used as a base for the interface and the film slab cell that will be placed on top of the substrate slab.\n" + "### 2.3. Create Substrate and Film Slabs\n", + "The substrate slab has `H_BN_LAYERS` layers of h-BN, the film slab one layer of graphene." ] }, { @@ -216,20 +180,16 @@ "\n", "substrate_slab_config = SlabConfiguration.from_parameters(\n", " material_or_dict=substrate,\n", - " miller_indices=SUBSTRATE_MILLER_INDICES,\n", - " number_of_layers=SUBSTRATE_THICKNESS,\n", + " miller_indices=(0, 0, 1),\n", + " number_of_layers=H_BN_LAYERS // 2, # two BN layers per bulk cell\n", " vacuum=0.0,\n", - " termination_top_formula=SUBSTRATE_TERMINATION_FORMULA,\n", - " use_conventional_cell=USE_CONVENTIONAL_CELL\n", ")\n", "\n", "film_slab_config = SlabConfiguration.from_parameters(\n", " material_or_dict=film,\n", - " miller_indices=FILM_MILLER_INDICES,\n", - " number_of_layers=FILM_THICKNESS,\n", + " miller_indices=(0, 0, 1),\n", + " number_of_layers=1,\n", " vacuum=0.0,\n", - " termination_top_formula=FILM_TERMINATION_FORMULA,\n", - " use_conventional_cell=USE_CONVENTIONAL_CELL\n", ")\n", "\n", "substrate_slab = SlabBuilder().get_material(substrate_slab_config)\n", @@ -240,129 +200,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Analyze possible interfaces with ZSL Analyzer\n", - "### 3.1. Initialize ZSL Analyzer\n", - "The search algorithm for supercells matching can be tuned by setting its parameters directly, otherwise the default values are used." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.made.tools.analyze.interface import ZSLInterfaceAnalyzer\n", - "\n", - "zsl_analyzer = ZSLInterfaceAnalyzer(\n", - " substrate_slab_configuration=substrate_slab_config,\n", - " film_slab_configuration=film_slab_config,\n", - " max_area=MAX_AREA,\n", - " max_area_ratio_tol=MAX_AREA_TOLERANCE,\n", - " max_length_tol=MAX_LENGTH_TOLERANCE,\n", - " max_angle_tol=MAX_ANGLE_TOLERANCE,\n", - " reduce_result_cell=False # Reduces supercell matrices in analyzer\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2. Generate matches with strain analyzer\n", - "Matches are sorted by size and strain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "matches = zsl_analyzer.zsl_match_holders" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.3. Plot matches by area and strain" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from mat3ra.notebooks_utils.ipython.entity.material.plot import plot_strain_vs_area\n", - "\n", - "PLOT_SETTINGS = {\n", - " \"HEIGHT\": 600,\n", - " \"X_SCALE\": \"log\", # or linear\n", - " \"Y_SCALE\": \"log\", # or linear\n", - "}\n", - "\n", - "plot_strain_vs_area(matches, PLOT_SETTINGS)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.4. Select the interface\n", - "\n", - "Select the index for the interface with the lowest strain and the smallest area." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "selected_index = 0\n", - "\n", - "from mat3ra.made.tools.helpers import create_interface_zsl_between_slabs as create_zsl_interface_between_slabs\n", - "\n", - "interface = create_zsl_interface_between_slabs(\n", - " substrate_slab=substrate_slab,\n", - " film_slab=film_slab,\n", - " gap=INTERFACE_DISTANCE,\n", - " vacuum=INTERFACE_VACUUM,\n", - " match_id=selected_index,\n", - " max_area=MAX_AREA,\n", - " max_area_ratio_tol=MAX_AREA_TOLERANCE,\n", - " max_length_tol=MAX_LENGTH_TOLERANCE,\n", - " max_angle_tol=MAX_ANGLE_TOLERANCE,\n", - " reduce_result_cell_to_primitive=REDUCE_RESULT_CELL_TO_PRIMITIVE,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Preview the selected interface and create variants\n", + "## 3. Create the interfaces\n", "\n", - "### 4.1. Preview the selected interface" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "visualize(interface, repetitions=[3, 3, 1])\n", - "visualize(interface, repetitions=[3, 3, 1], rotation=\"-90x\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.2. Shift film along y-axis\n", - "Shifting with the step of a/sqrt(3)/2 angstroms, where a is the lattice constant of the interface." + "### 3.1. Place graphene at each distance and shift it into each registry\n", + "The film is placed on the substrate slab at each of `DISTANCES` and shifted along y by `STACKING_SHIFTS` for each of `STACKINGS`; the registry of each carbon atom is measured on the result." ] }, { @@ -372,23 +213,54 @@ "outputs": [], "source": [ "import numpy as np\n", + "from mat3ra.made.tools.analyze.other import get_average_interlayer_distance\n", + "from mat3ra.made.tools.convert.interface_parts_enum import InterfacePartsEnum\n", + "from mat3ra.made.tools.helpers import create_interface_zsl_between_slabs as create_zsl_interface_between_slabs\n", "from mat3ra.made.tools.modify import interface_displace_part\n", "\n", - "a = interface.lattice.a\n", - "shifted_interfaces = []\n", - "for n in range(2, 9):\n", - " shifted_interface = interface_displace_part(\n", - " interface=interface,\n", - " displacement=[0, n * a / np.sqrt(3) / 2, 0],\n", - " use_cartesian_coordinates=True)\n", - " shifted_interfaces.append(shifted_interface)" + "\n", + "def get_registry(interface):\n", + " \"\"\"Atom of the top h-BN layer under each carbon atom, or \"hollow\" (Giovannetti et al. 2007 Fig. 1).\"\"\"\n", + " elements = np.array(interface.basis.elements.values)\n", + " coordinates = np.array(interface.basis.coordinates.values)\n", + " top_layer = (elements != \"C\") & np.isclose(coordinates[:, 2], coordinates[elements != \"C\", 2].max())\n", + " registry = []\n", + " for carbon in coordinates[elements == \"C\"]:\n", + " in_plane_offsets = (coordinates[top_layer, :2] - carbon[:2] + 0.5) % 1 - 0.5\n", + " atoms_below = elements[top_layer][np.all(np.abs(in_plane_offsets) < REGISTRY_TOLERANCE, axis=1)]\n", + " registry.append(atoms_below[0] if len(atoms_below) else \"hollow\")\n", + " return registry\n", + "\n", + "\n", + "interfaces = []\n", + "for stacking in STACKINGS:\n", + " for distance in DISTANCES:\n", + " # the builder adds the gap to the vacuum above the film\n", + " interface = create_zsl_interface_between_slabs(\n", + " substrate_slab=substrate_slab, film_slab=film_slab, gap=distance, vacuum=VACUUM - distance\n", + " )\n", + " interface = interface_displace_part(\n", + " interface=interface,\n", + " displacement=[0, STACKING_SHIFTS[stacking] * interface.lattice.a / np.sqrt(3), 0],\n", + " use_cartesian_coordinates=True,\n", + " )\n", + " interface.name = f\"Gr/hBN ({stacking}) d{distance:.2f}\"\n", + " interface.lattice.type = \"HEX\"\n", + " interfaces.append(interface)\n", + " interlayer_distance = get_average_interlayer_distance(\n", + " interface, InterfacePartsEnum.SUBSTRATE.value, InterfacePartsEnum.FILM.value\n", + " )\n", + " vacuum = interface.lattice.c * (1 - max(coordinate[2] for coordinate in interface.basis.coordinates.values))\n", + " print(f\"{interface.name}: {len(interface.basis.elements.ids)} atoms, a = {interface.lattice.a:.4f} Å, \"\n", + " f\"gamma = {interface.lattice.gamma:.1f}°, distance = {interlayer_distance:.3f} Å, \"\n", + " f\"vacuum = {vacuum:.2f} Å, C over {' / '.join(get_registry(interface))}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 4.3. Preview the shifted materials" + "### 3.2. Preview the interfaces" ] }, { @@ -397,14 +269,15 @@ "metadata": {}, "outputs": [], "source": [ - "visualize(shifted_interfaces, repetitions=[3, 3, 1])" + "visualize(interfaces, repetitions=[3, 3, 1])\n", + "visualize(interfaces, repetitions=[3, 3, 1], rotation=\"-90x\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 5. Pass data to the outside runtime" + "## 4. Pass data to the outside runtime" ] }, { @@ -416,9 +289,9 @@ "from mat3ra.notebooks_utils.io import download_content_to_file\n", "from mat3ra.notebooks_utils.material import set_materials\n", "\n", - "set_materials(shifted_interfaces)\n", - "for idx, shifted_interface in enumerate(shifted_interfaces):\n", - " download_content_to_file(shifted_interface.to_json(), f\"interface_{idx}.json\")" + "set_materials(interfaces)\n", + "for interface in interfaces:\n", + " download_content_to_file(interface.to_json(), f\"{interface.name}.json\")" ] } ], diff --git a/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene_SIMULATION.ipynb b/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene_SIMULATION.ipynb new file mode 100644 index 000000000..adc2cac6a --- /dev/null +++ b/other/materials_designer/specific_examples/interface_2d_2d_boron_nitride_graphene_SIMULATION.ipynb @@ -0,0 +1,873 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Substrate-Induced Band Gap of Graphene on h-BN\n", + "\n", + "> **Gianluca Giovannetti, Petr A. Khomyakov, Geert Brocks, Paul J. Kelly, and Jeroen van den Brink**\n", + "> **Substrate-induced band gap in graphene on hexagonal boron nitride: Ab initio density functional calculations**\n", + "> Physical Review B 76, 073103 (2007)\n", + "> [DOI: 10.1103/PhysRevB.76.073103](https://doi.org/10.1103/PhysRevB.76.073103)\n", + "\n", + "Calculate the total energy and the band structure of graphene on four layers of h-BN for the three\n", + "stackings and the list of graphene–h-BN distances created in the\n", + "[structure notebook](interface_2d_2d_boron_nitride_graphene.ipynb), using Quantum ESPRESSO and the\n", + "band structure + density of states workflow from Standata. The results reproduce Giovannetti et al.\n", + "(2007): Fig. 2 (total energy vs distance and the equilibrium distances), Fig. 3 (bands and density of\n", + "states of stacking (c) at its equilibrium distance, gap at K) and Fig. 4 (gap at K vs distance).\n", + "\n", + "

Usage

\n", + "\n", + "1. Create the materials in the [structure notebook](interface_2d_2d_boron_nitride_graphene.ipynb), which saves them to the `uploads` folder under the names built from `MATERIAL_NAME` in cell 1.2 below.\n", + "1. Set the stackings, distances and calculation parameters in cells 1.2 and 1.3, or keep the default values: the active stackings × distances run with them. Uncomment the rest of `STACKINGS` and `DISTANCES`, here and in the structure notebook, for the paper's full set.\n", + "1. Click \"Run\" > \"Run All\" to run all cells.\n", + "1. Wait for the jobs to complete.\n", + "1. Scroll down to view the results; section 8 prints the comparison with the paper.\n", + "\n", + "## Summary\n", + "\n", + "1. Set up the environment and parameters: install packages (JupyterLite only) and configure the stackings, distances, workflow, model, compute resources and jobs.\n", + "1. Authenticate and initialize API client: authenticate via browser, initialize the client, then select account and project.\n", + "1. Load the materials by name from the `uploads` folder, print their provenance and save them to the platform.\n", + "1. Configure the workflow: select the application and the model, load the band structure + DOS workflow from Standata, and set the computational parameters, the tetrahedron occupations and the dipole correction for each material.\n", + "1. Configure compute: get the list of clusters and create a compute configuration.\n", + "1. Run one job per material, one after another, re-using a job that already ran under the same workflow name.\n", + "1. Retrieve results: total energies, the direct gap at K, the equilibrium distance of each stacking, the total energy and the gap vs distance, the bands and density of states of (c) at its equilibrium distance, the h-BN gap and the effective mass at K.\n", + "1. Compare with Giovannetti et al. (2007)." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## 1. Set up the environment and parameters\n", + "### 1.1. Install packages (JupyterLite)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.packages import install_packages\n", + "\n", + "await install_packages(\"made|api_examples\")" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.2. Set parameters and configurations for the workflow and jobs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from mat3ra.ide.compute import QueueName\n", + "\n", + "ORGANIZATION_NAME = None # set to your organization name (full or partial); otherwise, your default one is used\n", + "FOLDER = \"./uploads\"\n", + "\n", + "# Giovannetti et al. 2007 compute all three stackings at every distance 2.5–3.9 Å; the defaults run\n", + "# stacking (c) at three distances around its minimum. Uncomment entries to compute more\n", + "# (one job per stacking × distance, ~25 min each on queue D with two cores).\n", + "STACKINGS = [\n", + " \"c\",\n", + " # \"a\",\n", + " # \"b\",\n", + "]\n", + "DISTANCES = [\n", + " 3.1, 3.2, 3.3,\n", + " # 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9,\n", + "]\n", + "MATERIAL_NAME = \"Gr/hBN ({stacking}) d{distance:.2f}\" # as saved by the structure notebook\n", + "\n", + "WORKFLOW_SEARCH_TERM = \"band_structure_dos.json\"\n", + "MY_WORKFLOW_NAME = \"Band Structure + DOS\"\n", + "APPLICATION_NAME = \"espresso\"\n", + "\n", + "CLUSTER_NAME = \"001\" # specify full or partial name i.e. \"cluster-001\" to select\n", + "QUEUE_NAME = QueueName.D\n", + "PPN = 2\n", + "TIME_LIMIT = \"04:00:00\"\n", + "\n", + "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n", + "POLL_INTERVAL = 60 # seconds" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.3. Set the DFT parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "MODEL_SUBTYPE = \"lda\"\n", + "FUNCTIONAL = \"pz\" # Giovannetti et al. 2007 use LDA: GGA gives essentially no interlayer binding\n", + "PSEUDOPOTENTIAL_TYPE = \"us\" # GBRV ultrasoft, the only LDA family the platform publishes for B, C and N\n", + "ECUTWFC = 40 # Ry, GBRV's tested cutoff; the paper's 600 eV is a VASP number\n", + "ECUTRHO = 200 # Ry, GBRV's tested charge-density cutoff\n", + "\n", + "KGRID = [36, 36, 1] # Giovannetti et al. 2007; a multiple of 3 keeps K on the mesh\n", + "OCCUPATIONS_SETTINGS = {\"occupations\": \"tetrahedra\"} # the paper's tetrahedron method\n", + "# eamp = 0: the sawtooth is the dipole correction only, no external field\n", + "DIPOLE_SETTINGS = {\"control\": {\"tefield\": True, \"dipfield\": True}, \"system\": {\"edir\": 3, \"eamp\": 0.0, \"eopreg\": 0.05}}\n", + "KPATH_STEPS = 100\n", + "MODEL_TAG = (f\"{FUNCTIONAL}-{PSEUDOPOTENTIAL_TYPE} {ECUTWFC}-{ECUTRHO}Ry k{KGRID[0]} p{KPATH_STEPS} \"\n", + " f\"{OCCUPATIONS_SETTINGS['occupations']} eamp{DIPOLE_SETTINGS['system']['eamp']}\")\n", + "\n", + "SCF_UNIT = \"pw_scf\"\n", + "NSCF_UNIT = \"pw_nscf\"\n", + "BANDS_UNIT = \"pw_bands\"\n", + "NUMBER_OF_OCCUPIED_BANDS = 20 # 40 valence electrons: C 4×2, B 3×4, N 5×4\n", + "\n", + "KPATH = [\n", + " {\"point\": \"Γ\", \"steps\": KPATH_STEPS},\n", + " {\"point\": \"K\", \"steps\": KPATH_STEPS},\n", + " {\"point\": \"M\", \"steps\": KPATH_STEPS},\n", + " {\"point\": \"Γ\", \"steps\": 1},\n", + "]\n", + "\n", + "VELOCITY_FIT_RANGE = (3, 8) # path points from K used for the ħv fit\n", + "ZOOM_POINTS = 12 # path points each side of K in the zoom\n", + "ZOOM_WINDOW = 0.5 # eV each side of the band edges" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## 2. Authenticate and initialize API client\n", + "### 2.1. Authenticate\n", + "Authenticate in the browser and have credentials stored in environment variable \"OIDC_ACCESS_TOKEN\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.auth import authenticate\n", + "\n", + "await authenticate()" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### 2.2. Initialize API client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.api_client import APIClient\n", + "\n", + "client = APIClient.authenticate()\n", + "client" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 2.3. Select account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "client.list_accounts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "selected_account = client.my_account\n", + "\n", + "if ORGANIZATION_NAME:\n", + " selected_account = client.get_account(name=ORGANIZATION_NAME)\n", + "\n", + "ACCOUNT_ID = selected_account.id\n", + "print(f\"✅ Selected account ID: {ACCOUNT_ID}, name: {selected_account.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### 2.4. Select project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "projects = client.projects.list({\"isDefault\": True, \"owner._id\": ACCOUNT_ID})\n", + "project_id = projects[0][\"_id\"]\n", + "print(f\"✅ Using project: {projects[0]['name']} ({project_id})\")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 3. Load the materials\n", + "### 3.1. Load from the uploads folder and print provenance\n", + "\n", + "The structures, their geometry and their lattice type all come from the structure notebook; this one\n", + "only loads them by name, one per stacking and distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.tools.analyze.other import get_average_interlayer_distance\n", + "from mat3ra.made.tools.convert.interface_parts_enum import InterfacePartsEnum\n", + "from mat3ra.notebooks_utils.core.entity.material.api import load_material\n", + "\n", + "material_names = {\n", + " stacking: {distance: MATERIAL_NAME.format(stacking=stacking, distance=distance) for distance in sorted(DISTANCES)}\n", + " for stacking in STACKINGS\n", + "}\n", + "materials = {}\n", + "for stacking, names in material_names.items():\n", + " for name in names.values():\n", + " material = load_material(client, FOLDER, name, ACCOUNT_ID)\n", + " materials[name] = material\n", + " interlayer_distance = get_average_interlayer_distance(\n", + " material, InterfacePartsEnum.SUBSTRATE.value, InterfacePartsEnum.FILM.value)\n", + " print(f\"{name}: {material.basis.number_of_atoms} atoms, a = {material.lattice.a:.4f} Å, \"\n", + " f\"gamma = {material.lattice.gamma:.3f}°, graphene–h-BN distance = {interlayer_distance:.3f} Å, \"\n", + " f\"stacking ({stacking})\")" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "### 3.2. Preview the materials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.material.visualize import visualize_materials\n", + "\n", + "visualize_materials([{\"material\": material, \"title\": name} for name, material in materials.items()],\n", + " viewer=\"wave\")" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### 3.3. Save the materials to the platform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.notebooks_utils.core.entity.material.api import get_or_create_material\n", + "\n", + "saved_materials = {}\n", + "for name, material in materials.items():\n", + " material.basis.set_labels_from_list([])\n", + " saved_materials[name] = Material.create(get_or_create_material(client, material, ACCOUNT_ID))" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## 4. Configure the workflow\n", + "### 4.1. Select application and model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.applications import ApplicationStandata\n", + "from mat3ra.standata.model_tree import ModelTreeStandata\n", + "from mat3ra.ade.application import Application\n", + "from mat3ra.mode import ModelFactory\n", + "\n", + "app_config = ApplicationStandata.get_by_name_first_match(APPLICATION_NAME)\n", + "app = Application(**app_config)\n", + "\n", + "model_config = ModelTreeStandata.get_model_by_parameters(type=\"dft\", subtype=MODEL_SUBTYPE, functional=FUNCTIONAL)\n", + "model_config[\"method\"] = {\"type\": \"pseudopotential\", \"subtype\": PSEUDOPOTENTIAL_TYPE}\n", + "model = ModelFactory.create(model_config)\n", + "print(f\"Using application: {app.name}, model: {MODEL_TAG}\")" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "### 4.2. Configure the workflow\n", + "\n", + "One workflow per material, all with the settings of 1.3. Each workflow is named after its material\n", + "and `MODEL_TAG`; section 6 finds a job that already ran by that name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.standata.workflows import WorkflowStandata\n", + "from mat3ra.wode.workflows import Workflow\n", + "from mat3ra.wode.context.providers import PlanewaveCutoffsContextProvider, PointsGridDataProvider, \\\n", + " PointsPathDataProvider\n", + "from mat3ra.notebooks_utils.workflow import patch_workflow_qe_input\n", + "\n", + "from copy import deepcopy\n", + "\n", + "workflow_config = WorkflowStandata.filter_by_application(app.name).get_by_name_first_match(WORKFLOW_SEARCH_TERM)\n", + "\n", + "cutoffs_context = PlanewaveCutoffsContextProvider(\n", + " wavefunction=ECUTWFC, density=ECUTRHO, isEdited=True).get_context_item_data()\n", + "grid_context = PointsGridDataProvider(\n", + " material=next(iter(materials.values())), dimensions=KGRID, isEdited=True).get_context_item_data()\n", + "path_context = PointsPathDataProvider(path=KPATH, isEdited=True).get_context_item_data()\n", + "\n", + "\n", + "def get_vacuum_center(material):\n", + " \"\"\"Crystal-z midpoint of the vacuum above the film, QE's `emaxpos` unit.\"\"\"\n", + " return (max(coordinate[2] for coordinate in material.basis.coordinates.values) + 1) / 2\n", + "\n", + "\n", + "workflows = {}\n", + "for name, material in materials.items():\n", + " workflow = Workflow.create(deepcopy(workflow_config))\n", + " workflow.name = f\"{MY_WORKFLOW_NAME} {name} {MODEL_TAG}\"\n", + " subworkflow = workflow.subworkflows[0]\n", + " subworkflow.model = model\n", + "\n", + " for unit_name, contexts in [(SCF_UNIT, [grid_context, cutoffs_context]),\n", + " (NSCF_UNIT, [grid_context, cutoffs_context]),\n", + " (BANDS_UNIT, [path_context, cutoffs_context])]:\n", + " unit = subworkflow.get_unit_by_name(name=unit_name)\n", + " for context in contexts:\n", + " unit.add_context(context)\n", + " subworkflow.set_unit(unit)\n", + " patch_workflow_qe_input(workflow, DIPOLE_SETTINGS, [SCF_UNIT, NSCF_UNIT, BANDS_UNIT])\n", + " patch_workflow_qe_input(workflow, {\"system\": OCCUPATIONS_SETTINGS}, [SCF_UNIT, NSCF_UNIT])\n", + " patch_workflow_qe_input(workflow, {\"system\": {\"emaxpos\": get_vacuum_center(material)}},\n", + " [SCF_UNIT, NSCF_UNIT, BANDS_UNIT])\n", + " workflows[name] = workflow\n", + " print(workflow.name)" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "### 4.3. Preview the workflow" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.workflow.visualize import visualize_workflow\n", + "\n", + "visualize_workflow(next(iter(workflows.values())))" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "### 4.4. Save the workflows to the collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.workflow.api import get_or_create_workflow\n", + "\n", + "for name, workflow in workflows.items():\n", + " saved = Workflow.create(get_or_create_workflow(client, workflow, ACCOUNT_ID))\n", + " print(f\"{name}: workflow {saved.id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## 5. Create the compute configuration\n", + "### 5.1. Get list of clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "clusters = client.clusters.list()\n", + "print(f\"Available clusters: {[c['hostname'] for c in clusters]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "### 5.2. Create the compute configuration for the jobs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.ide.compute import Compute\n", + "\n", + "if CLUSTER_NAME:\n", + " cluster = next((c for c in clusters if CLUSTER_NAME in c[\"hostname\"]), None)\n", + " if cluster is None:\n", + " raise ValueError(f\"Cluster '{CLUSTER_NAME}' not found. Available: {[c['hostname'] for c in clusters]}\")\n", + "else:\n", + " cluster = clusters[0]\n", + "compute = Compute(cluster=cluster, queue=QUEUE_NAME, ppn=PPN, timeLimit=TIME_LIMIT)\n", + "print(f\"Using cluster: {compute.cluster.hostname}, queue: {QUEUE_NAME}, ppn: {PPN}, \"\n", + " f\"time limit: {TIME_LIMIT}\")" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## 6. Run the jobs one at a time\n", + "\n", + "A job is found by its material and its workflow name, which carries the model tag, and re-used if it\n", + "exists; otherwise it is created and submitted. The jobs run one after another." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.job.api import find_job_for_material\n", + "from mat3ra.notebooks_utils.job import create_job\n", + "from mat3ra.notebooks_utils.api.job import submit_jobs, wait_for_jobs_to_finish_async\n", + "\n", + "job_ids = {}\n", + "for name, workflow in workflows.items():\n", + " job = find_job_for_material(\n", + " client, saved_materials[name].id, workflow.name, ACCOUNT_ID,\n", + " statuses=(\"submitted\", \"queued\", \"active\", \"finished\"),\n", + " )\n", + " if job is None:\n", + " job = create_job(\n", + " api_client=client, materials=[saved_materials[name]], workflow=workflow, project_id=project_id,\n", + " owner_id=ACCOUNT_ID, compute=compute.to_dict(), prefix=f\"{workflow.name} {timestamp}\",\n", + " )\n", + " submit_jobs(client.jobs, [job[\"_id\"]])\n", + " print(f\"✅ {name}: submitted job {job['_id']}\")\n", + " else:\n", + " print(f\"♻️ {name}: reusing job {job['_id']}\")\n", + " await wait_for_jobs_to_finish_async(client.jobs, [job[\"_id\"]], poll_interval=POLL_INTERVAL)\n", + " job_ids[name] = job[\"_id\"]" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "## 7. Retrieve the results\n", + "### 7.1. Total energies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.core.entity.property.api import get_properties_for_job\n", + "\n", + "total_energies = {}\n", + "for name, job_id in job_ids.items():\n", + " total_energies[name] = get_properties_for_job(client, job_id, \"total_energy\")[0][\"value\"]\n", + " print(f\"{name}: {total_energies[name]:.6f} eV\")" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "### 7.2. Direct gap at K\n", + "\n", + "The difference between the lowest unoccupied and the highest occupied band at K, the end of the first\n", + "leg of `KPATH`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "K_VERTEX_INDEX = KPATH_STEPS\n", + "\n", + "band_structures = {}\n", + "gaps = {}\n", + "for name, job_id in job_ids.items():\n", + " band_structures[name] = get_properties_for_job(client, job_id, property_name=\"band_structure\")[0]\n", + " bands_at_k = np.array(band_structures[name][\"yDataSeries\"])[:, K_VERTEX_INDEX]\n", + " gaps[name] = 1000 * (bands_at_k[NUMBER_OF_OCCUPIED_BANDS] - bands_at_k[NUMBER_OF_OCCUPIED_BANDS - 1])\n", + " print(f\"{name}: {gaps[name]:.1f} meV\")" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "### 7.3. Equilibrium distance of each stacking\n", + "\n", + "The vertex of the parabola through the three distances with the lowest total energy, and the gap at K\n", + "interpolated to it from 7.2. The bands of 7.6 (Fig. 3) are those of the distance in `DISTANCES`\n", + "nearest to it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "equilibrium_distances = {}\n", + "equilibrium_names = {}\n", + "gap_at_equilibrium = {}\n", + "for stacking, names in material_names.items():\n", + " lowest = sorted(names, key=lambda distance: total_energies[names[distance]])[:3]\n", + " quadratic, linear, _ = np.polyfit(lowest, [total_energies[names[distance]] for distance in lowest], 2)\n", + " equilibrium_distances[stacking] = -linear / (2 * quadratic)\n", + " nearest = min(names, key=lambda distance: abs(distance - equilibrium_distances[stacking]))\n", + " equilibrium_names[stacking] = names[nearest]\n", + " gap_at_equilibrium[stacking] = np.interp(\n", + " equilibrium_distances[stacking], list(names), [gaps[name] for name in names.values()])\n", + " print(f\"({stacking}): equilibrium distance {equilibrium_distances[stacking]:.3f} Å, \"\n", + " f\"gap at K interpolated to it {gap_at_equilibrium[stacking]:.1f} meV\")" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "### 7.4. Total energy vs distance (Fig. 2 of the paper)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "from mat3ra.notebooks_utils.plot import display_matplotlib_figure\n", + "\n", + "reference_stacking = \"c\" if \"c\" in STACKINGS else STACKINGS[0]\n", + "reference_energy = total_energies[material_names[reference_stacking][max(DISTANCES)]]\n", + "figure, axes = plt.subplots()\n", + "for stacking, names in material_names.items():\n", + " axes.plot(list(names), [total_energies[name] - reference_energy for name in names.values()], marker=\"o\",\n", + " label=f\"({stacking})\")\n", + "axes.set_xlabel(\"Graphene–h-BN distance (Å)\")\n", + "axes.set_ylabel(f\"E − E(({reference_stacking}), {max(DISTANCES):.2f} Å) (eV per cell)\")\n", + "axes.set_title(\"Total energy vs distance\")\n", + "axes.legend()\n", + "display_matplotlib_figure(figure)" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "### 7.5. Gap at K vs distance (Fig. 4 of the paper)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "figure, axes = plt.subplots()\n", + "for stacking, names in material_names.items():\n", + " axes.plot(list(names), [gaps[name] for name in names.values()], marker=\"o\", label=f\"({stacking})\")\n", + "axes.set_xlabel(\"Graphene–h-BN distance (Å)\")\n", + "axes.set_ylabel(\"Gap at K (meV)\")\n", + "axes.set_title(\"Gap at K vs distance\")\n", + "axes.legend()\n", + "display_matplotlib_figure(figure)" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "### 7.6. Bands and density of states of (c) at its equilibrium distance (Fig. 3 of the paper)\n", + "\n", + "The job at the distance nearest to the equilibrium of (c): band structure, density of states, and the\n", + "two bands around the gap near K." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "from mat3ra.notebooks_utils.ipython.entity.property.visualize import visualize_properties\n", + "\n", + "equilibrium_name_c = equilibrium_names[\"c\"]\n", + "visualize_properties(band_structures[equilibrium_name_c], title=f\"Band Structure: {equilibrium_name_c}\",\n", + " extra_config={\"material\": materials[equilibrium_name_c].to_dict()})\n", + "visualize_properties(get_properties_for_job(client, job_ids[equilibrium_name_c], property_name=\"density_of_states\"),\n", + " title=f\"Density of States: {equilibrium_name_c}\",\n", + " extra_config={\"material\": materials[equilibrium_name_c].to_dict()})\n", + "\n", + "K_STEP_GAMMA_K = 4 * np.pi / (3 * materials[equilibrium_name_c].lattice.a) / KPATH_STEPS # Å⁻¹ per Γ–K step\n", + "K_STEP_K_M = 2 * np.pi / (3 * materials[equilibrium_name_c].lattice.a) / KPATH_STEPS # Å⁻¹ per K–M step\n", + "\n", + "bands = np.array(band_structures[equilibrium_name_c][\"yDataSeries\"])\n", + "path_steps = np.arange(-ZOOM_POINTS, ZOOM_POINTS + 1)\n", + "k_offsets = path_steps * np.where(path_steps < 0, K_STEP_GAMMA_K, K_STEP_K_M)\n", + "figure, axes = plt.subplots()\n", + "for band in bands[NUMBER_OF_OCCUPIED_BANDS - 1 : NUMBER_OF_OCCUPIED_BANDS + 1]:\n", + " axes.plot(k_offsets, band[K_VERTEX_INDEX + path_steps])\n", + "axes.set_ylim(bands[NUMBER_OF_OCCUPIED_BANDS - 1, K_VERTEX_INDEX] - ZOOM_WINDOW,\n", + " bands[NUMBER_OF_OCCUPIED_BANDS, K_VERTEX_INDEX] + ZOOM_WINDOW)\n", + "axes.set_xlabel(\"k − K (Å⁻¹), Γ side < 0 < M side\")\n", + "axes.set_ylabel(\"Energy (eV)\")\n", + "axes.set_title(\"Bands around K\")\n", + "display_matplotlib_figure(figure)" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "### 7.7. h-BN gap at K" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "# the h-BN bands adjacent to the graphene pair\n", + "h_bn_gap = bands[NUMBER_OF_OCCUPIED_BANDS + 1, K_VERTEX_INDEX] - bands[NUMBER_OF_OCCUPIED_BANDS - 2, K_VERTEX_INDEX]\n", + "print(f\"{equilibrium_name_c}: h-BN gap at K {h_bn_gap:.2f} eV\")" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "### 7.8. Effective mass at K\n", + "\n", + "For a gapped Dirac cone, E(k) = ±√((Δ/2)² + (ħv k)²), the band-edge mass is m* = ħ²Δ / (2(ħv)²), with Δ\n", + "the gap at K from 7.2 and ħv the slope of the lowest unoccupied band against |k − K|, the mean of the\n", + "Γ–K and the K–M sides of K over `VELOCITY_FIT_RANGE`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "HBAR_SQUARED_OVER_ELECTRON_MASS = 7.6200 # eV·Å²\n", + "\n", + "fit_steps = np.arange(VELOCITY_FIT_RANGE[0], VELOCITY_FIT_RANGE[1] + 1)\n", + "conduction_band = bands[NUMBER_OF_OCCUPIED_BANDS]\n", + "hbar_velocity_gamma_k, _ = np.polyfit(K_STEP_GAMMA_K * fit_steps, conduction_band[K_VERTEX_INDEX - fit_steps], 1)\n", + "hbar_velocity_k_m, _ = np.polyfit(K_STEP_K_M * fit_steps, conduction_band[K_VERTEX_INDEX + fit_steps], 1)\n", + "hbar_velocity = (hbar_velocity_gamma_k + hbar_velocity_k_m) / 2\n", + "effective_mass = HBAR_SQUARED_OVER_ELECTRON_MASS * gaps[equilibrium_name_c] / 1000 / (2 * hbar_velocity**2)\n", + "print(f\"{equilibrium_name_c}: ħv = {hbar_velocity_gamma_k:.2f} eV·Å (Γ–K), {hbar_velocity_k_m:.2f} eV·Å (K–M), \"\n", + " f\"mean {hbar_velocity:.2f} eV·Å, effective mass at K {effective_mass:.2e} m_e\")" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "## 8. Compare with Giovannetti et al. (2007)\n", + "\n", + "The paper's equilibrium distances (Fig. 2) and gaps at equilibrium (Fig. 4, p. 3) for each active\n", + "stacking, then the h-BN gap and the effective mass at K for (c), printed beside the values above.\n", + "\n", + "Settings that differ from the paper's: GBRV ultrasoft pseudopotentials at 40/200 Ry against VASP at 600 eV." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "GIOVANNETTI_EQUILIBRIUM_DISTANCE = {\"a\": 3.50, \"b\": 3.40, \"c\": 3.22} # Å, Fig. 2\n", + "GIOVANNETTI_GAP_AT_EQUILIBRIUM = {\"a\": 56, \"b\": 46, \"c\": 53} # meV, Fig. 4 / p.3\n", + "GIOVANNETTI_H_BN_GAP_AT_K = 4.7 # eV, p.3\n", + "GIOVANNETTI_EFFECTIVE_MASS = 4.7e-3 # m_e, (c), p.4\n", + "\n", + "print(f\"{'stacking':>8} {'d_eq (Å)':>9} {'paper':>6} {'deviation':>9} {'gap at d_eq (meV)':>18} {'paper':>6} \"\n", + " f\"{'deviation':>9}\")\n", + "for stacking in STACKINGS:\n", + " distance = equilibrium_distances[stacking]\n", + " paper_distance = GIOVANNETTI_EQUILIBRIUM_DISTANCE[stacking]\n", + " gap = gap_at_equilibrium[stacking]\n", + " paper_gap = GIOVANNETTI_GAP_AT_EQUILIBRIUM[stacking]\n", + " print(f\"{stacking:>8} {distance:>9.3f} {paper_distance:>6.2f} \"\n", + " f\"{(distance - paper_distance) / paper_distance:>+9.1%} \"\n", + " f\"{gap:>18.1f} {paper_gap:>6} {(gap - paper_gap) / paper_gap:>+9.1%}\")\n", + "\n", + "print(f\"h-BN gap at K, (c): {h_bn_gap:.2f} eV, paper {GIOVANNETTI_H_BN_GAP_AT_K} eV\")\n", + "print(f\"Effective mass at K, (c): {effective_mass:.2e} m_e, paper {GIOVANNETTI_EFFECTIVE_MASS:.1e} m_e\")" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] G. Giovannetti, P. A. Khomyakov, G. Brocks, P. J. Kelly and J. van den Brink, \"Substrate-induced band gap in graphene on hexagonal boron nitride: Ab initio density functional calculations\", Phys. Rev. B 76, 073103 (2007). https://doi.org/10.1103/PhysRevB.76.073103" + ] + } + ], + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}