From fab7147830a56b327cc796a3cd3ab101f6267f14 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 11:41:37 -0700 Subject: [PATCH 01/21] First commit of query method with test --- src/mp_api/matproj.py | 227 ++++++++++++++++++++++++++++++----------- tests/test_mprester.py | 87 +++++++++++++++- 2 files changed, 256 insertions(+), 58 deletions(-) diff --git a/src/mp_api/matproj.py b/src/mp_api/matproj.py index 0a61cb00..a6bffc76 100644 --- a/src/mp_api/matproj.py +++ b/src/mp_api/matproj.py @@ -1,12 +1,14 @@ from os import environ import warnings -from collections import defaultdict +from typing import Optional, Tuple, List from enum import Enum, unique from pymatgen.core import Structure from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from pymatgen.core.surface import get_symmetrically_equivalent_miller_indices -from pymatgen.util.sequence import get_chunks +from pymatgen.analysis.magnetism import Ordering +from emmet.core.mpid import MPID +from emmet.core.symmetry import CrystalSystem from mp_api.core.client import BaseRester from mp_api.routes import * @@ -370,65 +372,176 @@ def get_phonon_bandstructure_by_material_id(self, material_id): def query( self, - criteria, - properties, - sort_field=None, - ascending=None, - num_chunks=None, - chunk_size=1000, - all_fields=True, - fields=None, + material_ids: Optional[List[MPID]] = None, + chemsys_formula: Optional[str] = None, + nsites: Optional[Tuple[int, int]] = None, + volume: Optional[Tuple[float, float]] = None, + density: Optional[Tuple[float, float]] = None, + crystal_system: Optional[CrystalSystem] = None, + spacegroup_number: Optional[int] = None, + spacegroup_symbol: Optional[str] = None, + deprecated: Optional[bool] = None, + total_energy: Optional[Tuple[float, float]] = None, + formation_energy: Optional[Tuple[float, float]] = None, + energy_above_hull: Optional[Tuple[float, float]] = None, + equillibrium_reaction_energy: Optional[Tuple[float, float]] = None, + uncorrected_energy: Optional[Tuple[float, float]] = None, + is_stable: Optional[bool] = None, + band_gap: Optional[Tuple[float, float]] = None, + efermi: Optional[Tuple[float, float]] = None, + is_gap_direct: Optional[bool] = None, + is_metal: Optional[bool] = None, + magnetic_ordering: Optional[Ordering] = None, + total_magnetization: Optional[Tuple[float, float]] = None, + total_magnetization_normalized_vol: Optional[Tuple[float, float]] = None, + total_magnetization_normalized_formula_units: Optional[ + Tuple[float, float] + ] = None, + k_voigt: Optional[Tuple[float, float]] = None, + k_reuss: Optional[Tuple[float, float]] = None, + k_vrh: Optional[Tuple[float, float]] = None, + g_voigt: Optional[Tuple[float, float]] = None, + g_reuss: Optional[Tuple[float, float]] = None, + g_vrh: Optional[Tuple[float, float]] = None, + elastic_anisotropy: Optional[Tuple[float, float]] = None, + poisson_ratio: Optional[Tuple[float, float]] = None, + e_total: Optional[Tuple[float, float]] = None, + e_ionic: Optional[Tuple[float, float]] = None, + e_static: Optional[Tuple[float, float]] = None, + n: Optional[Tuple[float, float]] = None, + piezoelectric_modulus: Optional[Tuple[float, float]] = None, + weighted_surface_energy: Optional[Tuple[float, float]] = None, + weighted_work_function: Optional[Tuple[float, float]] = None, + surface_anisotropy: Optional[Tuple[float, float]] = None, + shape_factor: Optional[Tuple[float, float]] = None, + has_props: Optional[List[str]] = None, + theoretical: Optional[bool] = None, + sort_field: Optional[str] = None, + ascending: Optional[bool] = None, + num_chunks: Optional[int] = None, + chunk_size: int = 1000, + all_fields: bool = True, + fields: Optional[List[str]] = None, ): - r""" - - Performs an advanced query using MongoDB-like syntax for directly - querying the Materials Project database. This allows one to perform - queries which are otherwise too cumbersome to perform using the standard - convenience methods. - - Please consult the Materials API documentation at - https://github.com/materialsproject/mapidoc, which provides a - comprehensive explanation of the document schema used in the Materials - Project (supported criteria and properties) and guidance on how best to - query for the relevant information you need. - - For queries that request data on more than CHUNK_SIZE materials at once, - this method will chunk a query by first retrieving a list of material - IDs that satisfy CRITERIA, and then merging the criteria with a - restriction to one chunk of materials at a time of size CHUNK_SIZE. You - can opt out of this behavior by setting CHUNK_SIZE=0. To guard against - intermittent server errors in the case of many chunks per query, - possibly-transient server errors will result in re-trying a give chunk - up to MAX_TRIES_PER_CHUNK times. + """ + Query core data using a variety of search criteria. - Args: - criteria (str/dict): Criteria of the query as a dictionary. - For example, {"elements":{"$in":["Li", - "Na", "K"], "$all": ["O"]}, "nelements":2} selects all Li, Na - and K oxides. {"band_gap": {"$gt": 1}} selects all materials - with band gaps greater than 1 eV. - properties (list): Properties to request for as a list. For - example, ["formula_pretty", "formation_energy_per_atom"] returns - the formula and formation energy per atom. - chunk_size (int): Number of materials for which to fetch data at a - time. More data-intensive properties may require smaller chunk - sizes. Use chunk_size=0 to force no chunking -- this is useful - when fetching only properties such as 'material_id'. - max_tries_per_chunk (int): How many times to re-try fetching a given - chunk when the server gives a 5xx error (e.g. a timeout error). - mp_decode (bool): Whether to do a decoding to a Pymatgen object - where possible. In some cases, it might be useful to just get - the raw python dict, i.e., set to False. + Arguments: + material_ids (List[MPID]): List of Materials Project IDs to return data for. + chemsys_formula (str): A chemical system (e.g., Li-Fe-O), + or formula including anonomyzed formula + or wild cards (e.g., Fe2O3, ABO3, Si*). + crystal_system (CrystalSystem): Crystal system of material. + spacegroup_number (int): Space group number of material. + spacegroup_symbol (str): Space group symbol of the material in international short symbol notation. + nsites (Tuple[int,int]): Minimum and maximum number of sites to consider. + volume (Tuple[float,float]): Minimum and maximum volume to consider. + density (Tuple[float,float]): Minimum and maximum density to consider. + deprecated (bool): Whether the material is tagged as deprecated. + total_energy (Tuple[int,int]): Minimum and maximum corrected total energy in eV/atom to consider. + formation_energy (Tuple[int,int]): Minimum and maximum formation energy in eV/atom to consider. + energy_above_hull (Tuple[int,int]): Minimum and maximum energy above the hull in eV/atom to consider. + uncorrected_energy (Tuple[int,int]): Minimum and maximum uncorrected total energy in eV/atom to consider. + band_gap (Tuple[float,float]): Minimum and maximum band gap in eV to consider. + efermi (Tuple[float,float]): Minimum and maximum fermi energy in eV to consider. + is_gap_direct (bool): Whether the material has a direct band gap. + is_metal (bool): Whether the material is considered a metal. + magnetic_ordering (Ordering): Magnetic ordering of the material. + total_magnetization (Tuple[float,float]): Minimum and maximum total magnetization values to consider. + total_magnetization_normalized_vol (Tuple[float,float]): Minimum and maximum total magnetization values + normalized by volume to consider. + total_magnetization_normalized_formula_units (Tuple[float,float]): Minimum and maximum total magnetization + values normalized by formula units to consider. + k_voigt (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Voigt average of the bulk modulus. + k_reuss (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Reuss average of the bulk modulus. + k_vrh (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Voigt-Reuss-Hill average of the bulk modulus. + g_voigt (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Voigt average of the shear modulus. + g_reuss (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Reuss average of the shear modulus. + g_vrh (Tuple[float,float]): Minimum and maximum value in GPa to consider for + the Voigt-Reuss-Hill average of the shear modulus. + elastic_anisotropy (Tuple[float,float]): Minimum and maximum value to consider for + the elastic anisotropy. + poisson_ratio (Tuple[float,float]): Minimum and maximum value to consider for + Poisson's ratio. + e_total (Tuple[float,float]): Minimum and maximum total dielectric constant to consider. + e_ionic (Tuple[float,float]): Minimum and maximum ionic dielectric constant to consider. + e_static (Tuple[float,float]): Minimum and maximum electronic dielectric constant to consider. + n (Tuple[float,float]): Minimum and maximum refractive index to consider. + piezoelectric_modulus (Tuple[float,float]): Minimum and maximum piezoelectric modulus to consider. + weighted_surface_energy (Tuple[float,float]): Minimum and maximum weighted surface energy in J/m² to + consider. + weighted_work_function (Tuple[float,float]): Minimum and maximum weighted work function in eV to consider. + surface_energy_anisotropy (Tuple[float,float]): Minimum and maximum surface energy anisotropy values to + consider. + shape_factor (Tuple[float,float]): Minimum and maximum shape factor values to consider. + has_props: (List[str]): The calculated properties available for the material. + theoretical: (bool): Whether the material is theoretical. + sort_field (str): Field used to sort results. + ascending (bool): Whether sorting should be in ascending order. + num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. + chunk_size (int): Number of data entries per chunk. + all_fields (bool): Whether to return all fields in the document. Defaults to True. + fields (List[str]): List of fields in SearchDoc to return data for. + Default is material_id if all_fields is False. Returns: - List of results. E.g., - [{u'composition': {u'O': 1, u'Li': 2.0}}, - {u'composition': {u'Na': 2.0, u'O': 2.0}}, - {u'composition': {u'K': 1, u'O': 3.0}}, - ...] - """ - # TODO: discuss - raise NotImplementedError + ([SearchDoc]) List of SearchDoc documents + """ + return self.search.search_docs( + material_ids=material_ids, + chemsys_formula=chemsys_formula, + nsites=nsites, + volume=volume, + density=density, + crystal_system=crystal_system, + spacegroup_number=spacegroup_number, + spacegroup_symbol=spacegroup_symbol, + deprecated=deprecated, + total_energy=total_energy, + formation_energy=formation_energy, + energy_above_hull=energy_above_hull, + equillibrium_reaction_energy=equillibrium_reaction_energy, + uncorrected_energy=uncorrected_energy, + is_stable=is_stable, + band_gap=band_gap, + efermi=efermi, + is_gap_direct=is_gap_direct, + is_metal=is_metal, + magnetic_ordering=magnetic_ordering, + total_magnetization=total_magnetization, + total_magnetization_normalized_vol=total_magnetization_normalized_vol, + total_magnetization_normalized_formula_units=total_magnetization_normalized_formula_units, + k_voigt=k_voigt, + k_reuss=k_reuss, + k_vrh=k_vrh, + g_voigt=g_voigt, + g_reuss=g_reuss, + g_vrh=g_vrh, + elastic_anisotropy=elastic_anisotropy, + poisson_ratio=poisson_ratio, + e_total=e_total, + e_ionic=e_ionic, + e_static=e_static, + n=n, + piezoelectric_modulus=piezoelectric_modulus, + weighted_surface_energy=weighted_surface_energy, + weighted_work_function=weighted_work_function, + surface_anisotropy=surface_anisotropy, + shape_factor=shape_factor, + has_props=has_props, + theoretical=theoretical, + sort_field=sort_field, + ascending=ascending, + num_chunks=num_chunks, + chunk_size=chunk_size, + all_fields=all_fields, + fields=fields, + ) def submit_structures(self, structures, public_name, public_email): """ diff --git a/tests/test_mprester.py b/tests/test_mprester.py index c92acbfa..44de6593 100644 --- a/tests/test_mprester.py +++ b/tests/test_mprester.py @@ -1,6 +1,8 @@ +from emmet.core.symmetry import CrystalSystem import pytest import random import os +import typing from mp_api.matproj import MPRester from mp_api.core.settings import MAPISettings import requests @@ -15,6 +17,7 @@ from pymatgen.core.periodic_table import Element from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine from pymatgen.io.vasp import Incar, Chgcar +from pymatgen.analysis.magnetism import Ordering api_is_up = ( requests.get("https://api.materialsproject.org/heartbeat").status_code == 200 @@ -28,7 +31,7 @@ def mpr(): rester.session.close() -# @pytest.mark.skipif(((os.environ.get("MP_API_KEY", None) is None) or (not api_is_up))) +@pytest.mark.skipif(((os.environ.get("MP_API_KEY", None) is None) or (not api_is_up))) class TestMPRester: def test_get_structure_by_material_id(self, mpr): s1 = mpr.get_structure_by_material_id("mp-1") @@ -158,3 +161,85 @@ def test_get_gb_data(self, mpr): assert gb_f.rotation_angle == pytest.approx(109.47122) assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) + + def test_query(self, mpr): + + excluded_params = [ + "sort_field", + "ascending", + "chunk_size", + "num_chunks", + "all_fields", + "fields", + "equillibrium_reaction_energy", + ] + + alt_name_dict = { + "material_ids": "material_id", + "chemsys_formula": "formula_pretty", + "piezoelectric_modulus": "e_ij_max", + "crystal_system": "symmetry", + "spacegroup_symbol": "symmetry", + "spacegroup_number": "symmetry", + "total_energy": "energy_per_atom", + "formation_energy": "formation_energy_per_atom", + "uncorrected_energy": "uncorrected_energy_per_atom", + } # type: dict + + custom_field_tests = { + "material_ids": ["mp-149"], + "chemsys_formula": "SiO2", + "crystal_system": CrystalSystem.cubic, + "spacegroup_number": 38, + "spacegroup_symbol": "Amm2", + "magnetic_ordering": Ordering.FM, + "has_props": ["dielectric"], + } # type: dict + + search_method = mpr.query + + # Get list of parameters + param_tuples = list(typing.get_type_hints(search_method).items()) + + # Query API for each numeric and bollean parameter and check if returned + for entry in param_tuples: + param = entry[0] + if param not in excluded_params: + param_type = entry[1].__args__[0] + q = None + + if param_type is typing.Tuple[int, int]: + project_field = alt_name_dict.get(param, None) + q = { + param: (-100, 100), + "chunk_size": 1, + "num_chunks": 1, + } + elif param_type is typing.Tuple[float, float]: + project_field = alt_name_dict.get(param, None) + q = { + param: (-100.12, 100.12), + "chunk_size": 1, + "num_chunks": 1, + } + elif param_type is bool: + project_field = alt_name_dict.get(param, None) + q = { + param: False, + "chunk_size": 1, + "num_chunks": 1, + } + elif param in custom_field_tests: + project_field = alt_name_dict.get(param, None) + q = { + param: custom_field_tests[param], + "chunk_size": 1, + "num_chunks": 1, + } + + doc = search_method(**q)[0].dict() + + assert ( + doc[project_field if project_field is not None else param] + is not None + ) From de70af45b5cdd3d65c4597b2abab8b0394dba049 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 11:46:57 -0700 Subject: [PATCH 02/21] Fix rester testing skipif --- tests/test_mprester.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_mprester.py b/tests/test_mprester.py index 44de6593..a738ce34 100644 --- a/tests/test_mprester.py +++ b/tests/test_mprester.py @@ -26,12 +26,15 @@ @pytest.fixture() def mpr(): - rester = MPRester() + rester = MPRester(endpoint="http://127.0.0.1:8000") yield rester rester.session.close() -@pytest.mark.skipif(((os.environ.get("MP_API_KEY", None) is None) or (not api_is_up))) +@pytest.mark.skipif( + ((os.environ.get("MP_API_KEY", None) is None) or (not api_is_up)), + reason="API is down", +) class TestMPRester: def test_get_structure_by_material_id(self, mpr): s1 = mpr.get_structure_by_material_id("mp-1") From 5c40db34f2376b0509b80888f8afbec3b33dcc4a Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 11:59:47 -0700 Subject: [PATCH 03/21] General query and search fixes and test update --- src/mp_api/routes/search/client.py | 6 +- tests/test_mprester.py | 284 +++++++++++++++-------------- 2 files changed, 150 insertions(+), 140 deletions(-) diff --git a/src/mp_api/routes/search/client.py b/src/mp_api/routes/search/client.py index a6d16dcf..6beedf67 100644 --- a/src/mp_api/routes/search/client.py +++ b/src/mp_api/routes/search/client.py @@ -159,13 +159,13 @@ def search_docs( "g_voigt": "g_voigt", "g_reuss": "g_reuss", "g_vrh": "g_vrh", - "elastic_anisotropy": "elastic_anisotropy", - "poisson_ratio": "poisson", + "elastic_anisotropy": "universal_anisotropy", + "poisson_ratio": "homogeneous_poisson", "e_total": "e_total", "e_ionic": "e_ionic", "e_static": "e_static", "n": "n", - "piezoelectric_modulus": "piezo_modulus", + "piezoelectric_modulus": "e_ij_max", "weighted_surface_energy": "weighted_surface_energy", "weighted_work_function": "weighted_work_function", "surface_energy_anisotropy": "surface_anisotropy", diff --git a/tests/test_mprester.py b/tests/test_mprester.py index a738ce34..de8f6744 100644 --- a/tests/test_mprester.py +++ b/tests/test_mprester.py @@ -36,134 +36,134 @@ def mpr(): reason="API is down", ) class TestMPRester: - def test_get_structure_by_material_id(self, mpr): - s1 = mpr.get_structure_by_material_id("mp-1") - assert s1.formula == "Cs2" - - s1 = mpr.get_structure_by_material_id("mp-4163", conventional_unit_cell=True) - assert s1.formula == "Ca12 Ti8 O28" - - s1 = mpr.get_structure_by_material_id("mp-149", final=False) - assert [s.formula for s in s1] == ["Si2"] - - # # requesting via task-id instead of mp-id - with pytest.warns(UserWarning): - mpr.get_structure_by_material_id("mp-698856") - - def test_get_database_version(self, mpr): - db_version = mpr.get_database_version() - assert db_version == MAPISettings().db_version - - def test_get_materials_id_from_task_id(self, mpr): - assert mpr.get_materials_id_from_task_id("mp-540081") == "mp-19017" - - # TODO: add method to MPRester - # def test_get_materials_id_references(self, mpr): - # data = mpr.get_materials_id_references("mp-123") - # assert len(data) > 1000 - - def test_get_materials_ids_doc(self, mpr): - mpids = mpr.get_materials_ids("Al2O3") - random.shuffle(mpids) - doc = mpr.materials.get_document_by_id(mpids.pop(0)) - assert doc.formula_pretty == "Al2O3" - - def test_get_structures(self, mpr): - structs = mpr.get_structures("Mn3O4") - assert len(structs) > 0 - - structs = mpr.get_structures("Mn3O4", final=False) - assert len(structs) > 0 - - def test_find_structure(self, mpr): - path = os.path.join(MAPISettings().test_files, "Si_mp_149.cif") - with open(path) as file: - data = mpr.find_structure(path) - assert len(data) > 0 - - s = CifParser(file).get_structures()[0] - data = mpr.find_structure(s) - assert len(data) > 0 - - def test_get_bandstructure_by_material_id(self, mpr): - bs = mpr.get_bandstructure_by_material_id("mp-149") - assert isinstance(bs, BandStructureSymmLine) - bs_unif = mpr.get_bandstructure_by_material_id("mp-149", line_mode=False) - assert isinstance(bs_unif, BandStructure) - assert not isinstance(bs_unif, BandStructureSymmLine) - - def test_get_dos_by_id(self, mpr): - dos = mpr.get_dos_by_material_id("mp-149") - assert isinstance(dos, CompleteDos) - - def test_get_entry_by_material_id(self, mpr): - e = mpr.get_entry_by_material_id("mp-19017") - assert isinstance(e[0], ComputedEntry) - assert e[0].composition.reduced_formula == "LiFePO4" - - def test_get_entries(self, mpr): - syms = ["Li", "Fe", "O"] - chemsys = "Li-Fe-O" - entries = mpr.get_entries(chemsys) - sorted_entries = mpr.get_entries(chemsys, sort_by_e_above_hull=True) - - elements = set([Element(sym) for sym in syms]) - for e in entries: - assert isinstance(e, ComputedEntry) - assert set(e.composition.elements).issubset(elements) - - assert sorted_entries != entries - - def test_get_phonon_data_by_material_id(self, mpr): - bs = mpr.get_phonon_bandstructure_by_material_id("mp-661") - assert isinstance(bs, PhononBandStructureSymmLine) - - def test_get_charge_density_data(self, mpr): - task_ids = mpr.get_charge_density_calculation_ids_from_material_id("mp-149") - assert len(task_ids) > 0 - - vasp_calc_details = mpr.get_charge_density_calculation_details( - task_ids[0]["task_id"] - ) - assert isinstance(vasp_calc_details.incar, Incar) - - chgcar = mpr.get_charge_density_from_calculation_id(task_ids[0]["task_id"]) - assert isinstance(chgcar, Chgcar) - - def test_get_substrates(self, mpr): - substrate_data = mpr.get_substrates("mp-123", [1, 0, 0]) - substrates = [sub_dict["sub_id"] for sub_dict in substrate_data] - assert "mp-2534" in substrates - - def test_get_surface_data(self, mpr): - data = mpr.get_surface_data("mp-126") # Pt - one_surf = mpr.get_surface_data("mp-129", miller_index=[-2, -3, 1]) - assert one_surf["surface_energy"] == pytest.approx(2.99156963) - assert one_surf["miller_index"] == pytest.approx([3, 2, 1]) - assert "surfaces" in data - surfaces = data["surfaces"] - assert len(surfaces) > 0 - surface = surfaces.pop() - assert "miller_index" in surface - assert "surface_energy" in surface - assert "is_reconstructed" in surface - assert "structure" in surface - - @pytest.mark.xfail # temporary - def test_get_gb_data(self, mpr): - mo_gbs = mpr.get_gb_data(chemsys="Mo") - assert len(mo_gbs) == 10 - mo_gbs_s5 = mpr.get_gb_data(pretty_formula="Mo", sigma=5) - assert len(mo_gbs_s5) == 3 - mo_s3_112 = mpr.get_gb_data( - material_id="mp-129", sigma=3, gb_plane=[1, -1, -2], - ) - assert len(mo_s3_112) == 1 - gb_f = mo_s3_112[0]["final_structure"] - assert gb_f.rotation_axis == pytest.approx([1, 1, 0]) - assert gb_f.rotation_angle == pytest.approx(109.47122) - assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) - assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) + # def test_get_structure_by_material_id(self, mpr): + # s1 = mpr.get_structure_by_material_id("mp-1") + # assert s1.formula == "Cs2" + + # s1 = mpr.get_structure_by_material_id("mp-4163", conventional_unit_cell=True) + # assert s1.formula == "Ca12 Ti8 O28" + + # s1 = mpr.get_structure_by_material_id("mp-149", final=False) + # assert [s.formula for s in s1] == ["Si2"] + + # # # requesting via task-id instead of mp-id + # with pytest.warns(UserWarning): + # mpr.get_structure_by_material_id("mp-698856") + + # def test_get_database_version(self, mpr): + # db_version = mpr.get_database_version() + # assert db_version == MAPISettings().db_version + + # def test_get_materials_id_from_task_id(self, mpr): + # assert mpr.get_materials_id_from_task_id("mp-540081") == "mp-19017" + + # # TODO: add method to MPRester + # # def test_get_materials_id_references(self, mpr): + # # data = mpr.get_materials_id_references("mp-123") + # # assert len(data) > 1000 + + # def test_get_materials_ids_doc(self, mpr): + # mpids = mpr.get_materials_ids("Al2O3") + # random.shuffle(mpids) + # doc = mpr.materials.get_document_by_id(mpids.pop(0)) + # assert doc.formula_pretty == "Al2O3" + + # def test_get_structures(self, mpr): + # structs = mpr.get_structures("Mn3O4") + # assert len(structs) > 0 + + # structs = mpr.get_structures("Mn3O4", final=False) + # assert len(structs) > 0 + + # def test_find_structure(self, mpr): + # path = os.path.join(MAPISettings().test_files, "Si_mp_149.cif") + # with open(path) as file: + # data = mpr.find_structure(path) + # assert len(data) > 0 + + # s = CifParser(file).get_structures()[0] + # data = mpr.find_structure(s) + # assert len(data) > 0 + + # def test_get_bandstructure_by_material_id(self, mpr): + # bs = mpr.get_bandstructure_by_material_id("mp-149") + # assert isinstance(bs, BandStructureSymmLine) + # bs_unif = mpr.get_bandstructure_by_material_id("mp-149", line_mode=False) + # assert isinstance(bs_unif, BandStructure) + # assert not isinstance(bs_unif, BandStructureSymmLine) + + # def test_get_dos_by_id(self, mpr): + # dos = mpr.get_dos_by_material_id("mp-149") + # assert isinstance(dos, CompleteDos) + + # def test_get_entry_by_material_id(self, mpr): + # e = mpr.get_entry_by_material_id("mp-19017") + # assert isinstance(e[0], ComputedEntry) + # assert e[0].composition.reduced_formula == "LiFePO4" + + # def test_get_entries(self, mpr): + # syms = ["Li", "Fe", "O"] + # chemsys = "Li-Fe-O" + # entries = mpr.get_entries(chemsys) + # sorted_entries = mpr.get_entries(chemsys, sort_by_e_above_hull=True) + + # elements = set([Element(sym) for sym in syms]) + # for e in entries: + # assert isinstance(e, ComputedEntry) + # assert set(e.composition.elements).issubset(elements) + + # assert sorted_entries != entries + + # def test_get_phonon_data_by_material_id(self, mpr): + # bs = mpr.get_phonon_bandstructure_by_material_id("mp-661") + # assert isinstance(bs, PhononBandStructureSymmLine) + + # def test_get_charge_density_data(self, mpr): + # task_ids = mpr.get_charge_density_calculation_ids_from_material_id("mp-149") + # assert len(task_ids) > 0 + + # vasp_calc_details = mpr.get_charge_density_calculation_details( + # task_ids[0]["task_id"] + # ) + # assert isinstance(vasp_calc_details.incar, Incar) + + # chgcar = mpr.get_charge_density_from_calculation_id(task_ids[0]["task_id"]) + # assert isinstance(chgcar, Chgcar) + + # def test_get_substrates(self, mpr): + # substrate_data = mpr.get_substrates("mp-123", [1, 0, 0]) + # substrates = [sub_dict["sub_id"] for sub_dict in substrate_data] + # assert "mp-2534" in substrates + + # def test_get_surface_data(self, mpr): + # data = mpr.get_surface_data("mp-126") # Pt + # one_surf = mpr.get_surface_data("mp-129", miller_index=[-2, -3, 1]) + # assert one_surf["surface_energy"] == pytest.approx(2.99156963) + # assert one_surf["miller_index"] == pytest.approx([3, 2, 1]) + # assert "surfaces" in data + # surfaces = data["surfaces"] + # assert len(surfaces) > 0 + # surface = surfaces.pop() + # assert "miller_index" in surface + # assert "surface_energy" in surface + # assert "is_reconstructed" in surface + # assert "structure" in surface + + # @pytest.mark.xfail # temporary + # def test_get_gb_data(self, mpr): + # mo_gbs = mpr.get_gb_data(chemsys="Mo") + # assert len(mo_gbs) == 10 + # mo_gbs_s5 = mpr.get_gb_data(pretty_formula="Mo", sigma=5) + # assert len(mo_gbs_s5) == 3 + # mo_s3_112 = mpr.get_gb_data( + # material_id="mp-129", sigma=3, gb_plane=[1, -1, -2], + # ) + # assert len(mo_s3_112) == 1 + # gb_f = mo_s3_112[0]["final_structure"] + # assert gb_f.rotation_axis == pytest.approx([1, 1, 0]) + # assert gb_f.rotation_angle == pytest.approx(109.47122) + # assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) + # assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) def test_query(self, mpr): @@ -174,7 +174,11 @@ def test_query(self, mpr): "num_chunks", "all_fields", "fields", - "equillibrium_reaction_energy", + "equillibrium_reaction_energy", # Fix (thermo model) + "weighted_work_function", # Fix (search builder) + "weighted_surface_energy", # Fix (search builder) + "surface_anisotropy", # Fix (search builder) + "shape_factor", # Fix (search builder) ] alt_name_dict = { @@ -187,6 +191,10 @@ def test_query(self, mpr): "total_energy": "energy_per_atom", "formation_energy": "formation_energy_per_atom", "uncorrected_energy": "uncorrected_energy_per_atom", + "magnetic_ordering": "ordering", + "elastic_anisotropy": "universal_anisotropy", + "poisson_ratio": "homogeneous_poisson", + "piezoelectric_modulus": "e_ij_max", } # type: dict custom_field_tests = { @@ -197,6 +205,7 @@ def test_query(self, mpr): "spacegroup_symbol": "Amm2", "magnetic_ordering": Ordering.FM, "has_props": ["dielectric"], + "theoretical": True, } # type: dict search_method = mpr.query @@ -208,34 +217,35 @@ def test_query(self, mpr): for entry in param_tuples: param = entry[0] if param not in excluded_params: + print(param) param_type = entry[1].__args__[0] q = None - if param_type is typing.Tuple[int, int]: + if param in custom_field_tests: project_field = alt_name_dict.get(param, None) q = { - param: (-100, 100), + param: custom_field_tests[param], "chunk_size": 1, "num_chunks": 1, } - elif param_type is typing.Tuple[float, float]: + elif param_type is typing.Tuple[int, int]: project_field = alt_name_dict.get(param, None) q = { - param: (-100.12, 100.12), + param: (-100, 100), "chunk_size": 1, "num_chunks": 1, } - elif param_type is bool: + elif param_type is typing.Tuple[float, float]: project_field = alt_name_dict.get(param, None) q = { - param: False, + param: (-100.12, 100.12), "chunk_size": 1, "num_chunks": 1, } - elif param in custom_field_tests: + elif param_type is bool: project_field = alt_name_dict.get(param, None) q = { - param: custom_field_tests[param], + param: False, "chunk_size": 1, "num_chunks": 1, } From aac15ff1c3a431476fb1428205fd8ccd94610015 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 12:00:31 -0700 Subject: [PATCH 04/21] Uncomment MPRester tests --- tests/test_mprester.py | 258 ++++++++++++++++++++--------------------- 1 file changed, 129 insertions(+), 129 deletions(-) diff --git a/tests/test_mprester.py b/tests/test_mprester.py index de8f6744..07dd532c 100644 --- a/tests/test_mprester.py +++ b/tests/test_mprester.py @@ -26,7 +26,7 @@ @pytest.fixture() def mpr(): - rester = MPRester(endpoint="http://127.0.0.1:8000") + rester = MPRester() yield rester rester.session.close() @@ -36,134 +36,134 @@ def mpr(): reason="API is down", ) class TestMPRester: - # def test_get_structure_by_material_id(self, mpr): - # s1 = mpr.get_structure_by_material_id("mp-1") - # assert s1.formula == "Cs2" - - # s1 = mpr.get_structure_by_material_id("mp-4163", conventional_unit_cell=True) - # assert s1.formula == "Ca12 Ti8 O28" - - # s1 = mpr.get_structure_by_material_id("mp-149", final=False) - # assert [s.formula for s in s1] == ["Si2"] - - # # # requesting via task-id instead of mp-id - # with pytest.warns(UserWarning): - # mpr.get_structure_by_material_id("mp-698856") - - # def test_get_database_version(self, mpr): - # db_version = mpr.get_database_version() - # assert db_version == MAPISettings().db_version - - # def test_get_materials_id_from_task_id(self, mpr): - # assert mpr.get_materials_id_from_task_id("mp-540081") == "mp-19017" - - # # TODO: add method to MPRester - # # def test_get_materials_id_references(self, mpr): - # # data = mpr.get_materials_id_references("mp-123") - # # assert len(data) > 1000 - - # def test_get_materials_ids_doc(self, mpr): - # mpids = mpr.get_materials_ids("Al2O3") - # random.shuffle(mpids) - # doc = mpr.materials.get_document_by_id(mpids.pop(0)) - # assert doc.formula_pretty == "Al2O3" - - # def test_get_structures(self, mpr): - # structs = mpr.get_structures("Mn3O4") - # assert len(structs) > 0 - - # structs = mpr.get_structures("Mn3O4", final=False) - # assert len(structs) > 0 - - # def test_find_structure(self, mpr): - # path = os.path.join(MAPISettings().test_files, "Si_mp_149.cif") - # with open(path) as file: - # data = mpr.find_structure(path) - # assert len(data) > 0 - - # s = CifParser(file).get_structures()[0] - # data = mpr.find_structure(s) - # assert len(data) > 0 - - # def test_get_bandstructure_by_material_id(self, mpr): - # bs = mpr.get_bandstructure_by_material_id("mp-149") - # assert isinstance(bs, BandStructureSymmLine) - # bs_unif = mpr.get_bandstructure_by_material_id("mp-149", line_mode=False) - # assert isinstance(bs_unif, BandStructure) - # assert not isinstance(bs_unif, BandStructureSymmLine) - - # def test_get_dos_by_id(self, mpr): - # dos = mpr.get_dos_by_material_id("mp-149") - # assert isinstance(dos, CompleteDos) - - # def test_get_entry_by_material_id(self, mpr): - # e = mpr.get_entry_by_material_id("mp-19017") - # assert isinstance(e[0], ComputedEntry) - # assert e[0].composition.reduced_formula == "LiFePO4" - - # def test_get_entries(self, mpr): - # syms = ["Li", "Fe", "O"] - # chemsys = "Li-Fe-O" - # entries = mpr.get_entries(chemsys) - # sorted_entries = mpr.get_entries(chemsys, sort_by_e_above_hull=True) - - # elements = set([Element(sym) for sym in syms]) - # for e in entries: - # assert isinstance(e, ComputedEntry) - # assert set(e.composition.elements).issubset(elements) - - # assert sorted_entries != entries - - # def test_get_phonon_data_by_material_id(self, mpr): - # bs = mpr.get_phonon_bandstructure_by_material_id("mp-661") - # assert isinstance(bs, PhononBandStructureSymmLine) - - # def test_get_charge_density_data(self, mpr): - # task_ids = mpr.get_charge_density_calculation_ids_from_material_id("mp-149") - # assert len(task_ids) > 0 - - # vasp_calc_details = mpr.get_charge_density_calculation_details( - # task_ids[0]["task_id"] - # ) - # assert isinstance(vasp_calc_details.incar, Incar) - - # chgcar = mpr.get_charge_density_from_calculation_id(task_ids[0]["task_id"]) - # assert isinstance(chgcar, Chgcar) - - # def test_get_substrates(self, mpr): - # substrate_data = mpr.get_substrates("mp-123", [1, 0, 0]) - # substrates = [sub_dict["sub_id"] for sub_dict in substrate_data] - # assert "mp-2534" in substrates - - # def test_get_surface_data(self, mpr): - # data = mpr.get_surface_data("mp-126") # Pt - # one_surf = mpr.get_surface_data("mp-129", miller_index=[-2, -3, 1]) - # assert one_surf["surface_energy"] == pytest.approx(2.99156963) - # assert one_surf["miller_index"] == pytest.approx([3, 2, 1]) - # assert "surfaces" in data - # surfaces = data["surfaces"] - # assert len(surfaces) > 0 - # surface = surfaces.pop() - # assert "miller_index" in surface - # assert "surface_energy" in surface - # assert "is_reconstructed" in surface - # assert "structure" in surface - - # @pytest.mark.xfail # temporary - # def test_get_gb_data(self, mpr): - # mo_gbs = mpr.get_gb_data(chemsys="Mo") - # assert len(mo_gbs) == 10 - # mo_gbs_s5 = mpr.get_gb_data(pretty_formula="Mo", sigma=5) - # assert len(mo_gbs_s5) == 3 - # mo_s3_112 = mpr.get_gb_data( - # material_id="mp-129", sigma=3, gb_plane=[1, -1, -2], - # ) - # assert len(mo_s3_112) == 1 - # gb_f = mo_s3_112[0]["final_structure"] - # assert gb_f.rotation_axis == pytest.approx([1, 1, 0]) - # assert gb_f.rotation_angle == pytest.approx(109.47122) - # assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) - # assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) + def test_get_structure_by_material_id(self, mpr): + s1 = mpr.get_structure_by_material_id("mp-1") + assert s1.formula == "Cs2" + + s1 = mpr.get_structure_by_material_id("mp-4163", conventional_unit_cell=True) + assert s1.formula == "Ca12 Ti8 O28" + + s1 = mpr.get_structure_by_material_id("mp-149", final=False) + assert [s.formula for s in s1] == ["Si2"] + + # # requesting via task-id instead of mp-id + with pytest.warns(UserWarning): + mpr.get_structure_by_material_id("mp-698856") + + def test_get_database_version(self, mpr): + db_version = mpr.get_database_version() + assert db_version == MAPISettings().db_version + + def test_get_materials_id_from_task_id(self, mpr): + assert mpr.get_materials_id_from_task_id("mp-540081") == "mp-19017" + + # TODO: add method to MPRester + # def test_get_materials_id_references(self, mpr): + # data = mpr.get_materials_id_references("mp-123") + # assert len(data) > 1000 + + def test_get_materials_ids_doc(self, mpr): + mpids = mpr.get_materials_ids("Al2O3") + random.shuffle(mpids) + doc = mpr.materials.get_document_by_id(mpids.pop(0)) + assert doc.formula_pretty == "Al2O3" + + def test_get_structures(self, mpr): + structs = mpr.get_structures("Mn3O4") + assert len(structs) > 0 + + structs = mpr.get_structures("Mn3O4", final=False) + assert len(structs) > 0 + + def test_find_structure(self, mpr): + path = os.path.join(MAPISettings().test_files, "Si_mp_149.cif") + with open(path) as file: + data = mpr.find_structure(path) + assert len(data) > 0 + + s = CifParser(file).get_structures()[0] + data = mpr.find_structure(s) + assert len(data) > 0 + + def test_get_bandstructure_by_material_id(self, mpr): + bs = mpr.get_bandstructure_by_material_id("mp-149") + assert isinstance(bs, BandStructureSymmLine) + bs_unif = mpr.get_bandstructure_by_material_id("mp-149", line_mode=False) + assert isinstance(bs_unif, BandStructure) + assert not isinstance(bs_unif, BandStructureSymmLine) + + def test_get_dos_by_id(self, mpr): + dos = mpr.get_dos_by_material_id("mp-149") + assert isinstance(dos, CompleteDos) + + def test_get_entry_by_material_id(self, mpr): + e = mpr.get_entry_by_material_id("mp-19017") + assert isinstance(e[0], ComputedEntry) + assert e[0].composition.reduced_formula == "LiFePO4" + + def test_get_entries(self, mpr): + syms = ["Li", "Fe", "O"] + chemsys = "Li-Fe-O" + entries = mpr.get_entries(chemsys) + sorted_entries = mpr.get_entries(chemsys, sort_by_e_above_hull=True) + + elements = set([Element(sym) for sym in syms]) + for e in entries: + assert isinstance(e, ComputedEntry) + assert set(e.composition.elements).issubset(elements) + + assert sorted_entries != entries + + def test_get_phonon_data_by_material_id(self, mpr): + bs = mpr.get_phonon_bandstructure_by_material_id("mp-661") + assert isinstance(bs, PhononBandStructureSymmLine) + + def test_get_charge_density_data(self, mpr): + task_ids = mpr.get_charge_density_calculation_ids_from_material_id("mp-149") + assert len(task_ids) > 0 + + vasp_calc_details = mpr.get_charge_density_calculation_details( + task_ids[0]["task_id"] + ) + assert isinstance(vasp_calc_details.incar, Incar) + + chgcar = mpr.get_charge_density_from_calculation_id(task_ids[0]["task_id"]) + assert isinstance(chgcar, Chgcar) + + def test_get_substrates(self, mpr): + substrate_data = mpr.get_substrates("mp-123", [1, 0, 0]) + substrates = [sub_dict["sub_id"] for sub_dict in substrate_data] + assert "mp-2534" in substrates + + def test_get_surface_data(self, mpr): + data = mpr.get_surface_data("mp-126") # Pt + one_surf = mpr.get_surface_data("mp-129", miller_index=[-2, -3, 1]) + assert one_surf["surface_energy"] == pytest.approx(2.99156963) + assert one_surf["miller_index"] == pytest.approx([3, 2, 1]) + assert "surfaces" in data + surfaces = data["surfaces"] + assert len(surfaces) > 0 + surface = surfaces.pop() + assert "miller_index" in surface + assert "surface_energy" in surface + assert "is_reconstructed" in surface + assert "structure" in surface + + @pytest.mark.xfail # temporary + def test_get_gb_data(self, mpr): + mo_gbs = mpr.get_gb_data(chemsys="Mo") + assert len(mo_gbs) == 10 + mo_gbs_s5 = mpr.get_gb_data(pretty_formula="Mo", sigma=5) + assert len(mo_gbs_s5) == 3 + mo_s3_112 = mpr.get_gb_data( + material_id="mp-129", sigma=3, gb_plane=[1, -1, -2], + ) + assert len(mo_s3_112) == 1 + gb_f = mo_s3_112[0]["final_structure"] + assert gb_f.rotation_axis == pytest.approx([1, 1, 0]) + assert gb_f.rotation_angle == pytest.approx(109.47122) + assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) + assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) def test_query(self, mpr): From f41c6861d7f2e0cd7137b8e917f3d51187201241 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:34:15 -0700 Subject: [PATCH 05/21] BS and DOS client fix and tests --- src/mp_api/routes/_consumer/client.py | 4 +- .../routes/electronic_structure/client.py | 10 -- tests/electronic_structure/test_client.py | 159 +++++++++++++++--- 3 files changed, 138 insertions(+), 35 deletions(-) diff --git a/src/mp_api/routes/_consumer/client.py b/src/mp_api/routes/_consumer/client.py index 5cb702c0..8c3d3f89 100644 --- a/src/mp_api/routes/_consumer/client.py +++ b/src/mp_api/routes/_consumer/client.py @@ -7,7 +7,7 @@ class UserSettingsRester(BaseRester): suffix = "user_settings" document_model = UserSettingsDoc # type: ignore - def set_user_settings(self, consumer_id, settings): # pragma: ignore + def set_user_settings(self, consumer_id, settings): # pragma: no cover """ Set user settings. Args: @@ -22,7 +22,7 @@ def set_user_settings(self, consumer_id, settings): # pragma: ignore body=settings, params={"consumer_id": consumer_id} ).get("data") - def get_user_settings(self, consumer_id, settings): # pragma: ignore + def get_user_settings(self, consumer_id, settings): # pragma: no cover """ Get user settings. Args: diff --git a/src/mp_api/routes/electronic_structure/client.py b/src/mp_api/routes/electronic_structure/client.py index 0a7f3082..f7c28a2c 100644 --- a/src/mp_api/routes/electronic_structure/client.py +++ b/src/mp_api/routes/electronic_structure/client.py @@ -263,8 +263,6 @@ def search_dos_summary( band_gap: Optional[Tuple[float, float]] = None, efermi: Optional[Tuple[float, float]] = None, magnetic_ordering: Optional[Ordering] = None, - is_gap_direct: bool = None, - is_metal: bool = None, num_chunks: Optional[int] = None, chunk_size: int = 1000, all_fields: bool = True, @@ -281,8 +279,6 @@ def search_dos_summary( band_gap (Tuple[float,float]): Minimum and maximum band gap in eV to consider. efermi (Tuple[float,float]): Minimum and maximum fermi energy in eV to consider. magnetic_ordering (Ordering): Magnetic ordering of the material. - is_gap_direct (bool): Whether the material has a direct band gap. - is_metal (bool): Whether the material is considered a metal. num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible. chunk_size (int): Number of data entries per chunk. all_fields (bool): Whether to return all fields in the document. Defaults to True. @@ -315,12 +311,6 @@ def search_dos_summary( if magnetic_ordering: query_params.update({"magnetic_ordering": magnetic_ordering.value}) - if is_gap_direct is not None: - query_params.update({"is_gap_direct": is_gap_direct}) - - if is_metal is not None: - query_params.update({"is_metal": is_metal}) - query_params = { entry: query_params[entry] for entry in query_params diff --git a/tests/electronic_structure/test_client.py b/tests/electronic_structure/test_client.py index e0b8b656..92312f92 100644 --- a/tests/electronic_structure/test_client.py +++ b/tests/electronic_structure/test_client.py @@ -1,13 +1,27 @@ import pytest -from mp_api.routes.electronic_structure.client import ElectronicStructureRester +from mp_api.routes.electronic_structure.client import ( + BandStructureRester, + DosRester, + ElectronicStructureRester, +) + +from mp_api.routes.electronic_structure.models.core import BSPathType, DOSProjectionType from pymatgen.analysis.magnetism import Ordering +from pymatgen.electronic_structure.core import Spin, OrbitalType + import inspect import typing -resters = [ElectronicStructureRester()] -excluded_params = [ +@pytest.fixture +def es_rester(): + rester = ElectronicStructureRester() + yield rester + rester.session.close() + + +es_excluded_params = [ "sort_field", "ascending", "chunk_size", @@ -18,16 +32,17 @@ sub_doc_fields = [] # type: list -alt_name_dict = {} # type: dict +es_alt_name_dict = {} # type: dict -custom_field_tests = {"magnetic_ordering": Ordering.FM} # type: dict +es_custom_field_tests = { + "magnetic_ordering": Ordering.FM, +} # type: dict -@pytest.mark.parametrize("rester", resters) -def test_client(rester): +def test_es_client(es_rester): # Get specific search method search_method = None - for entry in inspect.getmembers(rester, predicate=inspect.ismethod): + for entry in inspect.getmembers(es_rester, predicate=inspect.ismethod): if "search" in entry[0] and entry[0] != "search": search_method = entry[1] @@ -38,44 +53,142 @@ def test_client(rester): # Query API for each numeric and bollean parameter and check if returned for entry in param_tuples: param = entry[0] - if param not in excluded_params: + if param not in es_excluded_params: + param_type = entry[1].__args__[0] q = None - if param_type is typing.Tuple[int, int]: - project_field = alt_name_dict.get(param, None) + + if param in es_custom_field_tests: + project_field = es_alt_name_dict.get(param, None) + q = { + param: es_custom_field_tests[param], + "chunk_size": 1, + "num_chunks": 1, + } + elif param_type is typing.Tuple[int, int]: + project_field = es_alt_name_dict.get(param, None) q = { param: (-1000, 1000), "chunk_size": 1, "num_chunks": 1, } elif param_type is typing.Tuple[float, float]: - project_field = alt_name_dict.get(param, None) + project_field = es_alt_name_dict.get(param, None) q = { param: (-1000.1234, 1000.1234), "chunk_size": 1, "num_chunks": 1, } elif param_type is bool: - project_field = alt_name_dict.get(param, None) + project_field = es_alt_name_dict.get(param, None) q = { param: False, "chunk_size": 1, "num_chunks": 1, } - elif param in custom_field_tests: - project_field = alt_name_dict.get(param, None) - q = { - param: custom_field_tests[param], - "chunk_size": 1, - "num_chunks": 1, - } doc = search_method(**q)[0].dict() - for sub_field in sub_doc_fields: - if sub_field in doc: - doc = doc[sub_field] assert ( doc[project_field if project_field is not None else param] is not None ) + + +bs_custom_field_tests = { + "path_type": BSPathType.setyawan_curtarolo, + "magnetic_ordering": Ordering.FM, + "is_metal": True, + "is_gap_direct": True, + "efermi": (0, 100), + "band_gap": (0, 5), +} + +bs_sub_doc_fields = ["bandstructure"] + +bs_alt_name_dict = {"path_type": "setyawan_curtarolo"} # type: dict + + +@pytest.fixture +def bs_rester(): + rester = BandStructureRester() + yield rester + rester.session.close() + + +def test_bs_client(bs_rester): + # Get specific search method + search_method = None + for entry in inspect.getmembers(bs_rester, predicate=inspect.ismethod): + if "search" in entry[0] and entry[0] != "search": + search_method = entry[1] + + # Query fields + for param in bs_custom_field_tests: + project_field = bs_alt_name_dict.get(param, None) + q = { + param: bs_custom_field_tests[param], + "chunk_size": 1, + "num_chunks": 1, + } + doc = search_method(**q)[0].dict() + for sub_field in bs_sub_doc_fields: + if sub_field in doc: + doc = doc[sub_field] + + if param != "path_type": + doc = doc["setyawan_curtarolo"] + + assert doc[project_field if project_field is not None else param] is not None + + +dos_custom_field_tests = { + "projection_type": DOSProjectionType.total, + "magnetic_ordering": Ordering.FM, + "efermi": (0, 100), + "band_gap": (0, 5), +} + +dos_excluded_params = ["orbital", "element"] + +dos_sub_doc_fields = ["dos"] + +dos_alt_name_dict = { + "projection_type": "total", +} # type: dict + + +@pytest.fixture +def dos_rester(): + rester = DosRester() + yield rester + rester.session.close() + + +def test_dos_client(dos_rester): + # Get specific search method + search_method = None + for entry in inspect.getmembers(dos_rester, predicate=inspect.ismethod): + if "search" in entry[0] and entry[0] != "search": + search_method = entry[1] + + # Query fields + for param in dos_custom_field_tests: + if param not in dos_excluded_params: + project_field = dos_alt_name_dict.get(param, None) + q = { + param: dos_custom_field_tests[param], + "chunk_size": 1, + "num_chunks": 1, + } + doc = search_method(**q)[0].dict() + for sub_field in dos_sub_doc_fields: + if sub_field in doc: + doc = doc[sub_field] + + if param != "projection_type" and param != "magnetic_ordering": + doc = doc["total"]["1"] + + assert ( + doc[project_field if project_field is not None else param] is not None + ) From e7e839bb084fb1b5b17939e3a71dbbfc89407b97 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:36:40 -0700 Subject: [PATCH 06/21] Enhance robocrys test --- tests/robocrys/test_query_operators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/robocrys/test_query_operators.py b/tests/robocrys/test_query_operators.py index 388af988..4f98da21 100644 --- a/tests/robocrys/test_query_operators.py +++ b/tests/robocrys/test_query_operators.py @@ -58,3 +58,7 @@ def test_robocrys_search_query(): assert new_op.query(keywords="cubic, octahedra", skip=0, limit=10) == { "pipeline": pipeline } + + assert op.post_process([{"total_doc": 10}]) == [{"total_doc": 10}] + assert op.meta() == {"total_doc": 10} + From 22e3d430ffb279e0fcf106f46b42cccecb0f83c9 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:45:08 -0700 Subject: [PATCH 07/21] Robocrys client updates and tests --- src/mp_api/routes/robocrys/client.py | 11 +++++++--- tests/robocrys/test_client.py | 31 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 tests/robocrys/test_client.py diff --git a/src/mp_api/routes/robocrys/client.py b/src/mp_api/routes/robocrys/client.py index f6cf5253..b7dca7be 100644 --- a/src/mp_api/routes/robocrys/client.py +++ b/src/mp_api/routes/robocrys/client.py @@ -1,6 +1,6 @@ from typing import List -from mp_api.core.client import BaseRester +from mp_api.core.client import BaseRester, MPRestError from mp_api.routes.robocrys.models import RobocrysDoc @@ -24,7 +24,12 @@ def search_robocrys_text(self, keywords: List[str]): keyword_string = ",".join(keywords) robocrys_docs = self._query_resource( - criteria={"keywords": keyword_string}, suburl="text_search", use_document_model=True, - ) + criteria={"keywords": keyword_string}, + suburl="text_search", + use_document_model=True, + ).get("data", None) + + if robocrys_docs is None: + raise MPRestError("Cannot find any matches.") return robocrys_docs diff --git a/tests/robocrys/test_client.py b/tests/robocrys/test_client.py new file mode 100644 index 00000000..64849d3f --- /dev/null +++ b/tests/robocrys/test_client.py @@ -0,0 +1,31 @@ +import pytest +from mp_api.routes.robocrys.client import RobocrysRester + +import inspect +import typing +from pymatgen.core.periodic_table import Element + + +@pytest.fixture +def rester(): + rester = RobocrysRester() + yield rester + rester.session.close() + + +def test_client(rester): + # Get specific search method + search_method = None + for entry in inspect.getmembers(rester, predicate=inspect.ismethod): + if "search" in entry[0] and entry[0] != "search": + search_method = entry[1] + + if search_method is not None: + + q = {"keywords": ["silicon", "process"]} + + doc = search_method(**q)[0] + + assert doc.description is not None + assert doc.condensed_structure is not None + assert doc.task_id is not None From 430f93fc28a59e0eed92a4a41e72161c4a6d401b Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:47:07 -0700 Subject: [PATCH 08/21] EOS client test added --- tests/eos/__init__.py | 0 tests/eos/test_client.py | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 tests/eos/__init__.py create mode 100644 tests/eos/test_client.py diff --git a/tests/eos/__init__.py b/tests/eos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/eos/test_client.py b/tests/eos/test_client.py new file mode 100644 index 00000000..121a1771 --- /dev/null +++ b/tests/eos/test_client.py @@ -0,0 +1,81 @@ +import pytest +from mp_api.routes.eos.client import EOSRester + +import inspect +import typing + +resters = [EOSRester()] + +excluded_params = [ + "sort_field", + "ascending", + "chunk_size", + "num_chunks", + "all_fields", + "fields", +] + +sub_doc_fields = [] # type: list + +alt_name_dict = {} # type: dict + +custom_field_tests = {} # type: dict + + +@pytest.mark.parametrize("rester", resters) +def test_client(rester): + # Get specific search method + search_method = None + for entry in inspect.getmembers(rester, predicate=inspect.ismethod): + if "search" in entry[0] and entry[0] != "search": + search_method = entry[1] + + if search_method is not None: + # Get list of parameters + param_tuples = list(typing.get_type_hints(search_method).items()) + + # Query API for each numeric and bollean parameter and check if returned + for entry in param_tuples: + param = entry[0] + if param not in excluded_params: + param_type = entry[1].__args__[0] + q = None + + if param_type is typing.Tuple[int, int]: + project_field = alt_name_dict.get(param, None) + q = { + param: (-100, 100), + "chunk_size": 1, + "num_chunks": 1, + } + elif param_type is typing.Tuple[float, float]: + project_field = alt_name_dict.get(param, None) + q = { + param: (-100.12, 100.12), + "chunk_size": 1, + "num_chunks": 1, + } + elif param_type is bool: + project_field = alt_name_dict.get(param, None) + q = { + param: False, + "chunk_size": 1, + "num_chunks": 1, + } + elif param in custom_field_tests: + project_field = alt_name_dict.get(param, None) + q = { + param: custom_field_tests[param], + "chunk_size": 1, + "num_chunks": 1, + } + + doc = search_method(**q)[0].dict() + for sub_field in sub_doc_fields: + if sub_field in doc: + doc = doc[sub_field] + + assert ( + doc[project_field if project_field is not None else param] + is not None + ) From 66ce1415a1ec12796ea14eb8d9f67f453e0d7436 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:48:29 -0700 Subject: [PATCH 09/21] Redundant xas client methods removed --- src/mp_api/routes/xas/client.py | 36 +++++---------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/src/mp_api/routes/xas/client.py b/src/mp_api/routes/xas/client.py index bbd621f8..703c71ef 100644 --- a/src/mp_api/routes/xas/client.py +++ b/src/mp_api/routes/xas/client.py @@ -20,16 +20,6 @@ def get_available_elements( ): return [str(e) for e in Element] - def get_xas_doc(self, xas_id: str): - # TODO do some checking here for sub-components - query_params = {"all_fields": True} - - result = self._query_resource(query_params, suburl=xas_id) - if len(result.get("data", [])) > 0: - return result["data"][0] - else: - raise MPRestError("No document found") - def search_xas_docs( # TODO: add proper docstring self, @@ -64,26 +54,10 @@ def search_xas_docs( query_params.update({"ascending": ascending}) return super().search( - num_chunks=num_chunks, chunk_size=chunk_size, all_fields=all_fields, fields=fields, **query_params, + num_chunks=num_chunks, + chunk_size=chunk_size, + all_fields=all_fields, + fields=fields, + **query_params, ) - def count_xas_docs( - # TODO: switch to using core count method - self, - edge: Optional[Edge] = None, - absorbing_element: Optional[Element] = None, - required_elements: Optional[List[Element]] = None, - formula: Optional[str] = None, - ): - query_params = { - "edge": str(edge.value) if edge else None, - "absorbing_element": str(absorbing_element) if absorbing_element else None, - "formula": formula, - } - - if required_elements: - query_params["elements"] = ",".join([str(el) for el in required_elements]) - - query_params["limit"] = "1" - result = self._query_resource(query_params) - return result.get("meta", {}).get("total", 0) From d3947de36cd74ba5cfb5daa48bfc2d41f3a7ffae Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 14:54:22 -0700 Subject: [PATCH 10/21] Linting --- src/mp_api/matproj.py | 2 +- src/mp_api/routes/xas/client.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mp_api/matproj.py b/src/mp_api/matproj.py index a6bffc76..7a199713 100644 --- a/src/mp_api/matproj.py +++ b/src/mp_api/matproj.py @@ -492,7 +492,7 @@ def query( Returns: ([SearchDoc]) List of SearchDoc documents """ - return self.search.search_docs( + return self.search.search_docs( # type: ignore material_ids=material_ids, chemsys_formula=chemsys_formula, nsites=nsites, diff --git a/src/mp_api/routes/xas/client.py b/src/mp_api/routes/xas/client.py index 703c71ef..9020d1cf 100644 --- a/src/mp_api/routes/xas/client.py +++ b/src/mp_api/routes/xas/client.py @@ -60,4 +60,3 @@ def search_xas_docs( fields=fields, **query_params, ) - From e5dbee1af0af5f95291451a253ea971aef4a1dbb Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:08:36 -0700 Subject: [PATCH 11/21] Pragma no covers added to init and consumer --- src/mp_api/__init__.py | 4 ++-- src/mp_api/routes/_consumer/client.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mp_api/__init__.py b/src/mp_api/__init__.py index d464026a..04be9a90 100644 --- a/src/mp_api/__init__.py +++ b/src/mp_api/__init__.py @@ -15,7 +15,7 @@ settings = MAPISettings() -try: +try: # pragma: no cover if Path(settings.app_path).exists(): mapi = loadfn(settings.app_path) app = mapi.app @@ -24,7 +24,7 @@ if settings.debug: print(f"Failed loading App at {settings.app_path}") -except Exception as e: +except Exception as e: # pragma: no cover # Something went wrong with loading default app if settings.debug: print("Failed loading App") diff --git a/src/mp_api/routes/_consumer/client.py b/src/mp_api/routes/_consumer/client.py index 8c3d3f89..7fed3c30 100644 --- a/src/mp_api/routes/_consumer/client.py +++ b/src/mp_api/routes/_consumer/client.py @@ -2,7 +2,7 @@ from mp_api.core.client import BaseRester -class UserSettingsRester(BaseRester): +class UserSettingsRester(BaseRester): # pragma: no cover suffix = "user_settings" document_model = UserSettingsDoc # type: ignore From 37200f6f0ecac03a95720c42438c4ee77a734356 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:12:08 -0700 Subject: [PATCH 12/21] maggma added to setup.py requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 25680d29..19028214 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "requests>=2.23.0", "monty", "emmet-core", + "maggma", ], extras_require={ "server": [ From ae50c884356b9f2b00629e38bf1dbb1c0cbdf2cd Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:16:22 -0700 Subject: [PATCH 13/21] Pragma no cover added to init --- src/mp_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mp_api/__init__.py b/src/mp_api/__init__.py index 04be9a90..912418c4 100644 --- a/src/mp_api/__init__.py +++ b/src/mp_api/__init__.py @@ -6,7 +6,7 @@ try: __version__ = get_distribution(__name__).version -except DistributionNotFound: +except DistributionNotFound: # pragma: no cover # package is not installed __version__ = None # type: ignore From 81970c37345cbaf2797581b26de75b55791b340c Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:18:01 -0700 Subject: [PATCH 14/21] Pragma no covers added to core client --- src/mp_api/core/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mp_api/core/client.py b/src/mp_api/core/client.py index 6c25925b..d4657f1f 100644 --- a/src/mp_api/core/client.py +++ b/src/mp_api/core/client.py @@ -475,7 +475,7 @@ def _get_all_documents( return all_results - def query_by_task_id(self, *args, **kwargs): # pragma: ignore + def query_by_task_id(self, *args, **kwargs): # pragma: no cover print( "query_by_task_id has been renamed to get_document_by_id to be more general" ) @@ -505,10 +505,10 @@ def available_fields(self) -> List[str]: return ["Unknown fields."] return list(self.document_model.schema()["properties"].keys()) # type: ignore - def __repr__(self): # pragma: ignore + def __repr__(self): # pragma: no cover return f"<{self.__class__.__name__} {self.endpoint}>" - def __str__(self): # pragma: ignore + def __str__(self): # pragma: no cover if self.document_model is None: return self.__repr__() return ( From 1709f19ef2fa7c9c95ed8d3b5a901c3a33240df9 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:22:26 -0700 Subject: [PATCH 15/21] MPRester wulff shape test added --- src/mp_api/matproj.py | 6 +++--- tests/test_mprester.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mp_api/matproj.py b/src/mp_api/matproj.py index 7a199713..353cf6c4 100644 --- a/src/mp_api/matproj.py +++ b/src/mp_api/matproj.py @@ -207,13 +207,13 @@ def get_materials_id_from_task_id(self, task_id): materials_id (MPID) """ docs = self.materials.search(task_ids=[task_id], fields=["material_id"]) - if len(docs) == 1: + if len(docs) == 1: # pragma: no cover return str(docs[0].material_id) - elif len(docs) > 1: + elif len(docs) > 1: # pragma: no cover raise ValueError( f"Multiple documents return for {task_id}, this should not happen, please report it!" ) - else: + else: # pragma: no cover warnings.warn( f"No material found containing task {task_id}. Please report it if you suspect a task has gone missing." ) diff --git a/tests/test_mprester.py b/tests/test_mprester.py index 07dd532c..ca87aace 100644 --- a/tests/test_mprester.py +++ b/tests/test_mprester.py @@ -18,6 +18,7 @@ from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine from pymatgen.io.vasp import Incar, Chgcar from pymatgen.analysis.magnetism import Ordering +from pymatgen.analysis.wulff import WulffShape api_is_up = ( requests.get("https://api.materialsproject.org/heartbeat").status_code == 200 @@ -165,6 +166,10 @@ def test_get_gb_data(self, mpr): assert mo_s3_112[0]["gb_energy"] == pytest.approx(0.4796547330588574) assert mo_s3_112[0]["w_sep"] == pytest.approx(6.318144) + def test_get_wulff_shape(self, mpr): + ws = mpr.get_wulff_shape("mp-126") + assert isinstance(ws, WulffShape) + def test_query(self, mpr): excluded_params = [ From be93fe867e63eaa0f81e8a30ce5a481e78a5a789 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:23:51 -0700 Subject: [PATCH 16/21] Ignore consumer client in tests --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 8dea23e7..e9e7758c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,4 +4,5 @@ omit = */resources.py */models.py */models/* + */_consumer/client.py From 7e739db4d29bf26c501fa932b2f88da73cdc239f Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:26:26 -0700 Subject: [PATCH 17/21] Search ES query op test added --- tests/search/test_query_operators.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/search/test_query_operators.py b/tests/search/test_query_operators.py index 383c0a2a..1116381f 100644 --- a/tests/search/test_query_operators.py +++ b/tests/search/test_query_operators.py @@ -8,6 +8,7 @@ SearchMagneticQuery, SearchIsTheoreticalQuery, SearchStatsQuery, + SearchESQuery, ) from mp_api.routes.search.models import SearchStats @@ -95,3 +96,11 @@ def test_search_stats_query(): docs = [{"band_gap": 1}, {"band_gap": 2}, {"band_gap": 3}] assert isinstance(op.post_process(docs)[0], SearchStats) + + +def test_search_es_query(): + op = SearchESQuery() + + assert op.query(is_gap_direct=False, is_metal=False) == { + "criteria": {"is_gap_direct": False, "is_metal": False} + } From 5814ee28c2030d7ce9662db78f09f973a3fd758f Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:35:26 -0700 Subject: [PATCH 18/21] Synthesis client update and test added --- src/mp_api/routes/synthesis/client.py | 11 +++++----- tests/synthesis/test_client.py | 30 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 tests/synthesis/test_client.py diff --git a/src/mp_api/routes/synthesis/client.py b/src/mp_api/routes/synthesis/client.py index d5925c7e..81c0c3e0 100644 --- a/src/mp_api/routes/synthesis/client.py +++ b/src/mp_api/routes/synthesis/client.py @@ -1,4 +1,4 @@ -from mp_api.core.client import BaseRester +from mp_api.core.client import BaseRester, MPRestError from mp_api.routes.synthesis.models import SynthesisSearchResultModel from typing import List @@ -20,9 +20,10 @@ def search_synthesis_text(self, keywords: List[str]): keyword_string = ",".join(keywords) synthesis_docs = self._query_resource( - criteria={"keywords": keyword_string}, - suburl="text_search", - use_document_model=True, - ) + criteria={"keywords": keyword_string}, use_document_model=True, + ).get("data", None) + + if synthesis_docs is None: + raise MPRestError("Cannot find any matches.") return synthesis_docs diff --git a/tests/synthesis/test_client.py b/tests/synthesis/test_client.py new file mode 100644 index 00000000..69284c7f --- /dev/null +++ b/tests/synthesis/test_client.py @@ -0,0 +1,30 @@ +import pytest +from mp_api.routes.synthesis.client import SynthesisRester + +import inspect + + +@pytest.fixture +def rester(): + rester = SynthesisRester() + yield rester + rester.session.close() + + +def test_client(rester): + # Get specific search method + search_method = None + for entry in inspect.getmembers(rester, predicate=inspect.ismethod): + if "search" in entry[0] and entry[0] != "search": + search_method = entry[1] + + if search_method is not None: + + q = {"keywords": ["silicon"]} + + doc = search_method(**q)[0] + + assert doc.doi is not None + assert doc.paragraph_string is not None + assert doc.synthesis_type is not None + From be7f64756760d7e1be9d7a09371c47c5f4562c42 Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 15:51:45 -0700 Subject: [PATCH 19/21] Synth utils tests added --- test_files/synth_doc.json | 283 ++++++++++++++++++++++++++++++++++ tests/synthesis/test_utils.py | 37 +++++ 2 files changed, 320 insertions(+) create mode 100644 test_files/synth_doc.json create mode 100644 tests/synthesis/test_utils.py diff --git a/test_files/synth_doc.json b/test_files/synth_doc.json new file mode 100644 index 00000000..fab6d8fe --- /dev/null +++ b/test_files/synth_doc.json @@ -0,0 +1,283 @@ +{ + "doi": "10.1039/C2CE25672J", + "paragraph_string": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + "synthesis_type": "solid-state", + "reaction_string": "1 BiOCl + 1 Ca(NO3)2 + 2 [OH-] == 1 CaBiO2Cl + 1 H2O + 2 [NO3-]", + "reaction": { + "left_side": [ + { + "amount": "1", + "material": "Ca(NO3)2" + }, + { + "amount": "1", + "material": "BiOCl" + }, + { + "amount": "2", + "material": "[OH-]" + } + ], + "right_side": [ + { + "amount": "1", + "material": "CaBiO2Cl" + }, + { + "amount": "1", + "material": "H2O" + }, + { + "amount": "2", + "material": "[NO3-]" + } + ] + }, + "targets_formula": [ + "Ca1 Bi1 O2 Cl1" + ], + "target": { + "material_string": "CaBiO2Cl", + "material_name": "", + "material_formula": "CaBiO2Cl", + "phase": null, + "is_acronym": false, + "composition": [ + { + "formula": "CaBiO2Cl", + "amount": "1", + "elements": { + "Ca": "1", + "Bi": "1", + "O": "2", + "Cl": "1" + } + } + ], + "amounts_vars": {}, + "elements_vars": {}, + "additives": [], + "oxygen_deficiency": "None" + }, + "targets_formula_s": [ + "CaBiClO2" + ], + "precursors_formula_s": [ + "Ca(NO3)2", + "BiClO" + ], + "precursors_formula": [ + "Ca1 N2 O6", + "Bi1 O1 Cl1" + ], + "precursors": [ + { + "material_string": "Ca(NO3)2", + "material_name": "", + "material_formula": "Ca(NO3)2", + "phase": null, + "is_acronym": false, + "composition": [ + { + "formula": "Ca(NO3)2", + "amount": "1", + "elements": { + "N": "2", + "O": "6", + "Ca": "1" + } + } + ], + "amounts_vars": {}, + "elements_vars": {}, + "additives": [], + "oxygen_deficiency": "None" + }, + { + "material_string": "BiOCl", + "material_name": "", + "material_formula": "BiOCl", + "phase": null, + "is_acronym": false, + "composition": [ + { + "formula": "BiOCl", + "amount": "1", + "elements": { + "Bi": "1", + "O": "1", + "Cl": "1" + } + } + ], + "amounts_vars": {}, + "elements_vars": {}, + "additives": [], + "oxygen_deficiency": "None" + } + ], + "operations": [ + { + "type": "StartingSynthesis", + "token": "prepared", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "MixingOperation", + "token": "mixed", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "MixingOperation", + "token": "added", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "MixingOperation", + "token": "stirred", + "conditions": { + "heating_temperature": [], + "heating_time": [ + { + "min_value": 5.0, + "max_value": 5.0, + "values": [ + 5.0 + ], + "units": "h" + } + ], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "MixingOperation", + "token": "washed", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": "water", + "mixing_media": null + } + }, + { + "type": "DryingOperation", + "token": "dried", + "conditions": { + "heating_temperature": [ + { + "min_value": 80.0, + "max_value": 80.0, + "values": [ + 80.0 + ], + "units": "°C" + } + ], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "MixingOperation", + "token": "ground", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": "alumina", + "mixing_media": null + } + }, + { + "type": "HeatingOperation", + "token": "heated", + "conditions": { + "heating_temperature": [], + "heating_time": [ + { + "min_value": 12.0, + "max_value": 12.0, + "values": [ + 12.0 + ], + "units": "h" + } + ], + "heating_atmosphere": [ + "air" + ], + "mixing_device": null, + "mixing_media": null + } + }, + { + "type": "StartingSynthesis", + "token": "prepared", + "conditions": { + "heating_temperature": [], + "heating_time": [], + "heating_atmosphere": [], + "mixing_device": null, + "mixing_media": null + } + } + ], + "highlights": [ + { + "path": "paragraph_string", + "texts": [ + { + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", + "type": "text" + }, + { + "value": "silicon", + "type": "hit" + }, + { + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", + "type": "text" + } + ], + "score": 1.2976155281066895 + }, + { + "path": "paragraph_string", + "texts": [ + { + "value": "Silicon", + "type": "hit" + }, + { + "value": " powder was ...", + "type": "text" + } + ], + "score": 1.85878586769104 + } + ] +} \ No newline at end of file diff --git a/tests/synthesis/test_utils.py b/tests/synthesis/test_utils.py new file mode 100644 index 00000000..974caeed --- /dev/null +++ b/tests/synthesis/test_utils.py @@ -0,0 +1,37 @@ +import os +from json import load +from mp_api.routes.synthesis.utils import ( + make_ellipsis, + mask_paragraphs, + mask_highlights, +) +from mp_api import MAPISettings +from mp_api.routes.synthesis.models.core import SynthesisSearchResultModel + + +def test_make_ellipsis(): + text = "Lorem ipsum dolor sit amet" + altered_text = make_ellipsis(text, limit=10) + assert altered_text == "Lorem ..." + + altered_text = make_ellipsis(text, limit=10, remove_trailing=False) + assert altered_text == "... sit amet" + + +def test_mask_paragraphs(): + with open(os.path.join(MAPISettings().test_files, "synth_doc.json")) as file: + synth_doc = load(file) + + doc = SynthesisSearchResultModel(**synth_doc) + new_doc = mask_paragraphs(doc.dict(), limit=10) + + assert new_doc["paragraph_string"] == "Lorem ..." + + +def test_mask_highlights(): + with open(os.path.join(MAPISettings().test_files, "synth_doc.json")) as file: + synth_doc = load(file) + + doc = SynthesisSearchResultModel(**synth_doc) + new_doc = mask_highlights(doc.dict(), limit=10) + assert new_doc["highlights"][0]["texts"][0]["value"] == "... anim ..." From 70c558a013cac09ff3739526eff04a3a85431c6d Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 16:22:52 -0700 Subject: [PATCH 20/21] Flesh out client tests a little more --- src/mp_api/core/client.py | 14 +++++++------- tests/core/test_client.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/mp_api/core/client.py b/src/mp_api/core/client.py index d4657f1f..7f3a9cb8 100644 --- a/src/mp_api/core/client.py +++ b/src/mp_api/core/client.py @@ -25,7 +25,7 @@ try: from pymatgen.core import __version__ as pmg_version # type: ignore -except ImportError: +except ImportError: # pragma: no cover # fallback to root-level import for older pymatgen versions from pymatgen import __version__ as pmg_version # type: ignore @@ -112,13 +112,13 @@ def _create_session(api_key, include_user_agent): ) return session - def __enter__(self): + def __enter__(self): # pragma: no cover """ Support for "with" context. """ return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type, exc_val, exc_tb): # pragma: no cover """ Support for "with" context. """ @@ -330,7 +330,7 @@ def get_document_by_id( else: criteria = {"limit": 1} - if isinstance(fields, str): + if isinstance(fields, str): # pragma: no cover fields = (fields,) results = [] @@ -368,7 +368,7 @@ def get_document_by_id( if not results: raise MPRestError(f"No result for record {document_id}.") - elif len(results) > 1: + elif len(results) > 1: # pragma: no cover raise ValueError( f"Multiple records for {document_id}, this shouldn't happen. Please report as a bug." ) @@ -496,8 +496,8 @@ def count(self, criteria: Optional[Dict] = None) -> Union[int, str]: criteria=criteria, monty_decode=False ) # do not waste cycles Monty decoding return results["meta"]["total_doc"] - except Exception: - return "unknown" + except Exception: # pragma: no cover + return "Problem getting count" @property def available_fields(self) -> List[str]: diff --git a/tests/core/test_client.py b/tests/core/test_client.py index 26a17374..ea546478 100644 --- a/tests/core/test_client.py +++ b/tests/core/test_client.py @@ -36,6 +36,16 @@ def test_count(mpr): assert count == 1 +@pytest.mark.xfail +def test_get_document_no_id(mpr): + mpr.materials.get_document_by_id(None) + + +@pytest.mark.xfail +def test_get_document_no_doc(mpr): + mpr.materials.get_document_by_id("mp-1a") + + def test_available_fields(rester, mpr): assert len(mpr.materials.available_fields) > 0 assert rester.available_fields == ["Unknown fields."] From ca547026770febfc8806c4d2b7d9d216f6d285ad Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 1 Jul 2021 16:37:55 -0700 Subject: [PATCH 21/21] Limit warning removed from piezo client --- src/mp_api/routes/piezo/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/mp_api/routes/piezo/client.py b/src/mp_api/routes/piezo/client.py index c1af6e55..8f2ac455 100644 --- a/src/mp_api/routes/piezo/client.py +++ b/src/mp_api/routes/piezo/client.py @@ -43,10 +43,6 @@ def search_piezoelectric_docs( query_params = defaultdict(dict) # type: dict - if chunk_size <= 0 or chunk_size > 100: - warnings.warn("Improper chunk size given. Setting value to 100.") - chunk_size = 100 - if piezoelectric_modulus: query_params.update( {