Skip to content
Snippets Groups Projects
Commit abfc2801 authored by Lauri Himanen's avatar Lauri Himanen
Browse files

Added initial API routes for getting calculations and idealized structure related to a material.

parent bfcd9459
No related branches found
No related tags found
2 merge requests!123V0.8.1,!121Encyclopedia api
Pipeline #75370 passed
...@@ -22,7 +22,7 @@ from flask_restplus import Resource, abort, fields, marshal ...@@ -22,7 +22,7 @@ from flask_restplus import Resource, abort, fields, marshal
from flask import request from flask import request
from elasticsearch_dsl import Search, Q, A from elasticsearch_dsl import Search, Q, A
from nomad import config from nomad import config, files
from nomad.units import ureg from nomad.units import ureg
from nomad.atomutils import get_hill_decomposition from nomad.atomutils import get_hill_decomposition
from .api import api from .api import api
...@@ -50,13 +50,16 @@ material_prop_map = { ...@@ -50,13 +50,16 @@ material_prop_map = {
} }
def get_material(es_doc, keys): def get_es_doc_values(es_doc, mapping, keys=None):
"""Used to form a material definition for "materials/<material_id>" from """Used to form a material definition for "materials/<material_id>" from
the given ElasticSearch root document. the given ElasticSearch root document.
""" """
if keys is None:
keys = mapping.keys()
result = {} result = {}
for key in keys: for key in keys:
es_key = material_prop_map[key] es_key = mapping[key]
try: try:
value = es_doc value = es_doc
for part in es_key.split("."): for part in es_key.split("."):
...@@ -82,6 +85,7 @@ material_result = api.model("material_result", { ...@@ -82,6 +85,7 @@ material_result = api.model("material_result", {
"formula": fields.String, "formula": fields.String,
"formula_reduced": fields.String, "formula_reduced": fields.String,
"system_type": fields.String, "system_type": fields.String,
"n_matches": fields.Integer,
# Bulk only # Bulk only
"has_free_wyckoff_parameters": fields.String, "has_free_wyckoff_parameters": fields.String,
"strukturbericht_designation": fields.String, "strukturbericht_designation": fields.String,
...@@ -149,7 +153,7 @@ class EncMaterialResource(Resource): ...@@ -149,7 +153,7 @@ class EncMaterialResource(Resource):
# Create result JSON # Create result JSON
entry = response[0] entry = response[0]
result = get_material(entry, keys) result = get_es_doc_values(entry, material_prop_map, keys)
return result, 200 return result, 200
...@@ -166,7 +170,7 @@ materials_query = api.model("materials_input", { ...@@ -166,7 +170,7 @@ materials_query = api.model("materials_input", {
"page": fields.Integer(default=1), "page": fields.Integer(default=1),
"per_page": fields.Integer(default=25), "per_page": fields.Integer(default=25),
"pagination": fields.Boolean, "pagination": fields.Boolean,
"mode": fields.String(default="collapse"), "mode": fields.String(default="aggregation"),
})), })),
"material_name": fields.List(fields.String), "material_name": fields.List(fields.String),
"structure_type": fields.List(fields.String), "structure_type": fields.List(fields.String),
...@@ -184,15 +188,17 @@ materials_query = api.model("materials_input", { ...@@ -184,15 +188,17 @@ materials_query = api.model("materials_input", {
"code_name": fields.List(fields.String), "code_name": fields.List(fields.String),
"mass_density": fields.Nested(range_query, description="Mass density range in kg / m ** 3."), "mass_density": fields.Nested(range_query, description="Mass density range in kg / m ** 3."),
}) })
materials_result = api.model("materials_result", { pages_result = api.model("page_info", {
"total_results": fields.Integer(allow_null=False),
"results": fields.List(fields.Nested(material_result)),
"pages": fields.Nested(api.model("page_info", {
"per_page": fields.Integer, "per_page": fields.Integer,
"total": fields.Integer, "total": fields.Integer,
"page": fields.Integer, "page": fields.Integer,
"pages": fields.Integer, "pages": fields.Integer,
})), })
materials_result = api.model("materials_result", {
"total_results": fields.Integer(allow_null=False),
"results": fields.List(fields.Nested(material_result)),
"pages": fields.Nested(pages_result),
"es_query": fields.String(allow_null=False), "es_query": fields.String(allow_null=False),
}) })
...@@ -346,7 +352,7 @@ class EncMaterialsResource(Resource): ...@@ -346,7 +352,7 @@ class EncMaterialsResource(Resource):
# 1: The paginated approach: No way to know the amount of matches, # 1: The paginated approach: No way to know the amount of matches,
# but can return aggregation results in a quick fashion including # but can return aggregation results in a quick fashion including
# the number of matches entries per material. # the number of matches entries per material.
if mode == "aggregate": if mode == "aggregation":
after = None after = None
# The loop is awkward, but emulates the old behaviour until the GUI is adapted. # The loop is awkward, but emulates the old behaviour until the GUI is adapted.
for _ in range(page): for _ in range(page):
...@@ -389,8 +395,8 @@ class EncMaterialsResource(Resource): ...@@ -389,8 +395,8 @@ class EncMaterialsResource(Resource):
keys = list(material_prop_map.keys()) keys = list(material_prop_map.keys())
for material in materials: for material in materials:
representative = material["representative"][0] representative = material["representative"][0]
mat_dict = get_material(representative, keys) mat_dict = get_es_doc_values(representative, material_prop_map, keys)
mat_dict["n_of_calculations"] = material.doc_count mat_dict["n_matches"] = material.doc_count
result_list.append(mat_dict) result_list.append(mat_dict)
# Page information is incomplete for aggregations # Page information is incomplete for aggregations
...@@ -421,7 +427,7 @@ class EncMaterialsResource(Resource): ...@@ -421,7 +427,7 @@ class EncMaterialsResource(Resource):
result_list = [] result_list = []
keys = list(material_prop_map.keys()) keys = list(material_prop_map.keys())
for material in response: for material in response:
mat_result = get_material(material, keys) mat_result = get_es_doc_values(material, material_prop_map, keys)
result_list.append(mat_result) result_list.append(mat_result)
# Full page information available for collapse # Full page information available for collapse
...@@ -442,7 +448,7 @@ class EncMaterialsResource(Resource): ...@@ -442,7 +448,7 @@ class EncMaterialsResource(Resource):
group_result = api.model("group_result", { group_result = api.model("group_result", {
"calculation_list": fields.List(fields.String), "calculations_list": fields.List(fields.String),
"energy_minimum": fields.Float, "energy_minimum": fields.Float,
"group_hash": fields.String, "group_hash": fields.String,
"group_type": fields.String, "group_type": fields.String,
...@@ -467,7 +473,7 @@ class EncGroupsResource(Resource): ...@@ -467,7 +473,7 @@ class EncGroupsResource(Resource):
@api.response(400, "Bad request") @api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw) @api.response(200, "Metadata send", fields.Raw)
@api.expect(material_query, validate=False) @api.expect(material_query, validate=False)
@api.marshal_with(groups_result, skip_none=True) @api.marshal_with(groups_result)
@api.doc("enc_materials") @api.doc("enc_materials")
def get(self, material_id): def get(self, material_id):
...@@ -522,12 +528,9 @@ class EncGroupsResource(Resource): ...@@ -522,12 +528,9 @@ class EncGroupsResource(Resource):
# No hits on the top query level # No hits on the top query level
response = s.execute() response = s.execute()
n_hits = response.hits.total groups = []
if n_hits == 0:
abort(404, message="The specified material could not be found.")
# Collect information for each group from the aggregations # Collect information for each group from the aggregations
groups = []
groups_eos = response.aggs.groups_eos.buckets groups_eos = response.aggs.groups_eos.buckets
groups_param = response.aggs.groups_param.buckets groups_param = response.aggs.groups_param.buckets
...@@ -539,7 +542,7 @@ class EncGroupsResource(Resource): ...@@ -539,7 +542,7 @@ class EncGroupsResource(Resource):
"group_type": group_type, "group_type": group_type,
"nr_of_calculations": len(calculations), "nr_of_calculations": len(calculations),
"representative_calculation_id": hits[0].calc_id, "representative_calculation_id": hits[0].calc_id,
"calculation_list": calculations, "calculations_list": calculations,
"energy_minimum": hits[0].encyclopedia.properties.energies.energy_total, "energy_minimum": hits[0].encyclopedia.properties.energies.energy_total,
} }
return group_dict return group_dict
...@@ -555,3 +558,250 @@ class EncGroupsResource(Resource): ...@@ -555,3 +558,250 @@ class EncGroupsResource(Resource):
"total_groups": len(groups), "total_groups": len(groups),
} }
return result, 200 return result, 200
suggestions_query = api.parser()
suggestions_query.add_argument(
"property",
type=str,
choices=("code_name", "structure_type"),
help="The property name for which suggestions are returned.",
location="args"
)
suggestions_result = api.model("suggestions_result", {
"code_name": fields.List(fields.String),
"structure_type": fields.List(fields.String),
})
@ns.route("/suggestions")
class EncSuggestionsResource(Resource):
@api.response(404, "Suggestion not found")
@api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw)
@api.expect(suggestions_query, validate=False)
@api.marshal_with(suggestions_result, skip_none=True)
@api.doc("enc_suggestions")
def get(self):
# Parse request arguments
args = suggestions_query.parse_args()
prop = args.get("property", None)
return {prop: []}, 200
calc_query = api.parser()
calc_query.add_argument(
"page",
default=0,
type=int,
help="The page number to return.",
location="args"
)
calc_query.add_argument(
"per_page",
default=25,
type=int,
help="The number of results per page",
location="args"
)
calc_prop_map = {
"calc_id": "calc_id",
"code_name": "dft.code_name",
"code_version": "dft.code_version",
"functional_type": "encyclopedia.method.functional_type",
"basis_set_type": "dft.basis_set",
"run_type": "encyclopedia.calculation.calculation_type",
"has_dos": "encyclopedia.properties.electronic_dos",
"has_band_structure": "encyclopedia.properties.electronic_band_structure",
"has_thermal_properties": "encyclopedia.properties.thermodynamical_properties",
}
calculation_result = api.model("calculation_result", {
"calc_id": fields.String,
"code_name": fields.String,
"code_version": fields.String,
"functional_type": fields.String,
"basis_set_type": fields.String,
"run_type": fields.String,
"has_dos": fields.Boolean,
"has_band_structure": fields.Boolean,
"has_thermal_properties": fields.Boolean,
})
calculations_result = api.model("calculations_result", {
"total_results": fields.Integer,
"pages": fields.Nested(pages_result),
"results": fields.List(fields.Nested(calculation_result)),
})
@ns.route("/materials/<string:material_id>/calculations")
class EncCalculationResource(Resource):
@api.response(404, "Suggestion not found")
@api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw)
@api.expect(calc_query, validate=False)
@api.doc("enc_calculations")
def get(self, material_id):
"""Used to return all calculations related to the given material.
"""
args = calc_query.parse_args()
page = args["page"]
per_page = args["per_page"]
s = Search(index=config.elastic.index_name)
query = Q(
"bool",
filter=[
Q("term", published=True),
Q("term", with_embargo=False),
Q("term", encyclopedia__material__material_id=material_id),
]
)
s = s.query(query)
# The query is filtered already on the ES side so we don"t need to
# transfer so much data.
s = s.extra(**{
"_source": {"includes": list(calc_prop_map.values())},
"size": per_page,
"from": page,
})
response = s.execute()
# No such material
if len(response) == 0:
abort(404, message="There is no material {}".format(material_id))
# Create result JSON
results = []
for entry in response:
calc_dict = get_es_doc_values(entry, calc_prop_map)
calc_dict["has_dos"] = calc_dict["has_dos"] is not None
calc_dict["has_band_structure"] = calc_dict["has_dos"] is not None
calc_dict["has_thermal_properties"] = calc_dict["has_thermal_properties"] is not None
results.append(calc_dict)
result = {
"total_results": len(results),
"results": results,
"pages": {
"per_page": per_page,
"page": page,
}
}
return result, 200
@ns.route("/materials/<string:material_id>/cells")
class EncCellsResource(Resource):
@api.response(404, "Suggestion not found")
@api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw)
@api.doc("enc_cells")
def get(self, material_id):
"""Used to return cell information related to the given material.
"""
return {"results": []}, 200
wyckoff_variables_result = api.model("wyckoff_variables_result", {
"x": fields.Float,
"y": fields.Float,
"z": fields.Float,
})
wyckoff_set_result = api.model("wyckoff_set_result", {
"wyckoff_letter": fields.String,
"indices": fields.List(fields.Integer),
"element": fields.String,
"variables": fields.List(fields.Nested(wyckoff_variables_result)),
})
idealized_structure_result = api.model("idealized_structure_result", {
"atom_labels": fields.List(fields.String),
"atom_positions": fields.List(fields.List(fields.Float)),
"lattice_vectors": fields.List(fields.List(fields.Float)),
"lattice_vectors_primitive": fields.List(fields.List(fields.Float)),
"lattice_parameters": fields.List(fields.Float),
"periodicity": fields.List(fields.Boolean),
"number_of_atoms": fields.Integer,
"cell_volume": fields.Float,
"wyckoff_sets": fields.List(fields.Nested(wyckoff_set_result)),
})
@ns.route("/materials/<string:material_id>/idealized_structure")
class EncIdealizedStructureResource(Resource):
@api.response(404, "Suggestion not found")
@api.response(400, "Bad request")
@api.response(200, "Metadata send", fields.Raw)
@api.marshal_with(idealized_structure_result, skip_none=True)
@api.doc("enc_material_idealized_structure")
def get(self, material_id):
"""Specialized path for returning a representative idealized structure
that is displayed in the gui for this material.
"""
# The representative idealized structure simply comes from the first
# calculation when the calculations are alphabetically sorted by their
# calc_id. Coming up with a good way to select the representative one
# is pretty tricky in general, there are several options:
# - Lowest energy: This would be most intuitive, but the energy scales
# between codes do not match, and the energy may not have been
# reported.
# - Volume that is closest to mean volume: how to calculate volume for
# molecules, surfaces, etc...
# - Random: We would want the representative visualization to be
# relatively stable.
s = Search(index=config.elastic.index_name)
query = Q(
"bool",
filter=[
Q("term", published=True),
Q("term", with_embargo=False),
Q("term", encyclopedia__material__material_id=material_id),
]
)
s = s.query(query)
# The query is filtered already on the ES side so we don"t need to
# transfer so much data.
s = s.extra(**{
"sort": [{"calc_id": {"order": "asc"}}],
"_source": {"includes": ["upload_id", "calc_id"]},
"size": 1,
})
response = s.execute()
# No such material
if len(response) == 0:
abort(404, message="There is no material {}".format(material_id))
# Read the idealized_structure from the Archive. The structure can be
# quite large and no direct search queries are performed against it, so
# it is not in the ES index.
entry = response[0]
upload_id = entry.upload_id
calc_id = entry.calc_id
idealized_structure = read_archive(upload_id, calc_id, 'section_metadata/encyclopedia/material/idealized_structure')
return idealized_structure, 200
def read_archive(upload_id, calc_id, path):
"""Used to read data from the archive.
"""
upload_files = files.UploadFiles.get(upload_id)
# upload_files_cache[upload_id] = upload_files
with upload_files.read_archive(calc_id) as archive:
data = archive[calc_id]
parts = path.split("/")
for part in parts:
data = data[part]
if not isinstance(data, dict):
data = data.to_dict()
return data
...@@ -332,25 +332,29 @@ class Method(MSection): ...@@ -332,25 +332,29 @@ class Method(MSection):
type=MEnum("DFT", "GW", "unavailable", DFTU="DFT+U"), type=MEnum("DFT", "GW", "unavailable", DFTU="DFT+U"),
description=""" description="""
Generic name for the used methodology. Generic name for the used methodology.
""" """,
a_search=Search()
) )
core_electron_treatment = Quantity( core_electron_treatment = Quantity(
type=MEnum("full all electron", "all electron frozen core", "pseudopotential", "unavailable"), type=MEnum("full all electron", "all electron frozen core", "pseudopotential", "unavailable"),
description=""" description="""
How the core electrons are described. How the core electrons are described.
""" """,
a_search=Search()
) )
functional_long_name = Quantity( functional_long_name = Quantity(
type=str, type=str,
description=""" description="""
Full identified for the used exchange-correlation functional. Full identified for the used exchange-correlation functional.
""" """,
a_search=Search()
) )
functional_type = Quantity( functional_type = Quantity(
type=str, type=str,
description=""" description="""
Basic type of the used exchange-correlation functional. Basic type of the used exchange-correlation functional.
""" """,
a_search=Search()
) )
method_hash = Quantity( method_hash = Quantity(
type=str, type=str,
...@@ -430,7 +434,8 @@ class Calculation(MSection): ...@@ -430,7 +434,8 @@ class Calculation(MSection):
unavailable="unavailable"), unavailable="unavailable"),
description=""" description="""
Defines the type of calculation that was detected for this entry. Defines the type of calculation that was detected for this entry.
""" """,
a_search=Search()
) )
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment