diff --git a/docs/api.md b/docs/api.md index 6b55af62ae75dd0bcee56ffec58376ec7b7a9797..04c5f5d2774b00c709edbec37bf3d6117427b9cf 100644 --- a/docs/api.md +++ b/docs/api.md @@ -397,7 +397,7 @@ curl "{{ nomad_url() }}/v1/entries/raw/download?results.material.elements=Ti&res ## Access archives Above under [getting started](#getting started), you'll already learned how to access -archive data. A speciality of archive API functions is that you can define is `required` +archive data. A speciality of archive API functions is that you can define what is `required` from the archives. ``` @@ -419,4 +419,6 @@ response = requests.post( }) ``` -{{ doc_snippet('archive-required') }} \ No newline at end of file +{{ doc_snippet('archive-required') }} + +{{ metainfo_data() }} \ No newline at end of file diff --git a/docs/pythonlib.md b/docs/pythonlib.md index 514ba87cdb8d7829971e61fc987065ba1f11151e..9ce5ec085ad7d9b84820450fce4b933d20fa3221 100644 --- a/docs/pythonlib.md +++ b/docs/pythonlib.md @@ -1,3 +1,202 @@ -# The NOMAD Python package -- nomad-lab +# Using the Python library -comming soon ... \ No newline at end of file +NOMAD provides a Python package called `nomad-lab`. +## Install + +The package is hosted on [pypi](https://pypi.org/project/nomad-lab/) +and you can install it with *pip* (or conda). + +To install the latest stable pypi release, simply use pip: +```sh +pip install nomad-lab +``` + +Since NOMAD v1 is still in beta, this will still give you the Python library for +the NOMAD v0.10.x version. + +To install the latest release developer release (e.g. v1) from our servers use: +```sh +pip install nomad-lab --extra-index-url https://gitlab.mpcdf.mpg.de/api/v4/projects/2187/packages/pypi/simple +``` + +There are different layers of dependencies that you have to install, in order to use certain functions of NOMAD. +The base install above, will only install the necessary packages for +accessing the NOMAD Archive and use the NOMAD metainfo (see access the archive). + +Other functions, e.g. using the NOMAD parsers to parse your code output, require additional dependencies. +You can use the [extra] notation to install these extra requirements: + +``` +pip install nomad-lab[parsing] +pip install nomad-lab[infrastructure] +pip install nomad-lab[dev] +pip install nomad-lab[all] +``` +The various extras have the following meaning: + +- *parsing*, everything necessary to run the parsers + +- *infrastructure*, everything to run NOMAD services + +- *dev*, additional tools that are necessary to develop NOMAD + +- *all*, all of the above + +## Access parsed NOMAD data with `ArchiveQuery` + +The `ArchiveQuery` allows you to search for entries and access their parsed *archive* data +at the same time. Furthermore, all data is accessible through a convenient Python interface +based on the [NOMAD metainfo](metainfo.html) rather than plain JSON. + +Here is an example: +```py +query = ArchiveQuery( + query={ + 'results.method.simulation.program_name': 'VASP', + 'results.material.elements': ['Ti', 'O'], + 'results.method.simulation.geometry_optimization': { + 'convergence_tolerance_energy_difference:lt': 1e-22 + } + }, + required={ + 'workflow': { + 'calculation_result_ref': { + 'energy': '*', + 'system_ref': { + 'chemical_composition_reduced': '*' + } + } + } + }, + parallel=10, + max=100) +``` + +This instantiates an `ArchiveQuery`. You can print some details about the query: + +```py +print(query) +``` + +This gives you a general overview about the query. For example what search is used on +the NOMAD API. How many entries were found. What was already downloaded, etc. +``` +Query: { + "and": [ + { + "results.method.simulation.program_name": "VASP", + "results.material.elements": [ + "Ti", + "O" + ], + "results.method.simulation.geometry_optimization": { + "convergence_tolerance_energy_difference:lt": 1e-22 + } + }, + { + "quantities": [ + "run.system.chemical_composition_reduced", + "run.calculation.system_ref", + "run.calculation.energy", + "workflow", + "workflow.calculation_result_ref" + ] + } + ] +} +Total number of entries that fulfil the query: 252 +Number queried entries: 252 +Number of entries loaded in the last api call: 70 +Bytes loaded in the last api call: 53388 +Bytes loaded from this query: 53388 +Number of downloaded entries: 70 +Number of made api calls: 1 +``` + +This `ArchiveQuery` is not downloaded all archive data immediately. More and more data +will be downloaded as you iterate through the query: +```py +for result in query: + calc = result.workflow[0].calculation_result_ref + formula = calc.system_ref.chemical_composition_reduced + total_energy = calc.energy.total.value.to(units.eV) + print(f'{formula}: {total_energy}') +``` + +The resulting output can look like this: +``` +O10K2Ti3La2: -136.76387842 electron_volt +Li2O10Ti3La2: -139.15455203 electron_volt +O8Ti4: -107.30373862 electron_volt +O8Ca2Ti4: -116.52240913000001 electron_volt +... +``` + +Let's discuss the used `ArchiveQuery` parameters: + +- `query`, this is an arbitrary API query as discussed in the under [Queries in the API section](api.html#queries). +- `required`, this optional parameter allows you to specify what parts of an archive you need. This is also +described in under [Access archives in API section](api.html#access-archives). +- `per_page`, with this optional parameter you can determine, how many results are downloaded at a time. For bulk downloading many results, we recommend ~100. If you are just interested in the first results a lower number might increase performance. +- `max`, with this optional parameter, we limit the maximum amount of entries that are downloaded, just to avoid accidentally iterating through a result set of unknown and potentially large size. +- `owner` and `auth`, allows you to access private data or specify you only want to +query your data. See also [owner](api.html#owner) and [auth](api.html#authentication) in the API section. Her is an example with authentication: +```py +from nomad.client import ArchiveQuery, Auth + +query = ArchiveQuery( + owner='user', + required={ + 'run': { + 'system[-1]': '*' + } + }, + authentication=Auth(user='yourusername', password='yourpassword')) +``` + +The archive query object can be treated as a Python list-like. You use indices and ranges to select results. Each result is a Python object. The attributes of these objects are +determined by NOMAD's schema, [the metainfo and it's Python interface](metainfo). +This energy value is a number with an attached unit (Joule), which can be converted to something else (e.g. eV). {{ metainfo_data() }} + +The create query object keeps all results in memory. Keep this in mind, when you are accessing a large amount of query results. + +## Use NOMAD parser locally + +If you install `nomad-lab[parsers]`, you can use the NOMAD parsers locally on your computer. +To use the NOMAD parsers from the command line, you can use the parse CLI command. The parse command will automatically match the right parser to your code output file and run the parser. There are two output formats, `--show-metadata` (a JSON representation of the basic metadata), `--show-archive` (a JSON representation of the full parse results). + +```sh +nomad parser --show-archive <path-to-your-mainfile-code-output-file> +``` + +You can also use the NOMAD parsers from within Python. This will give you the parse results as metainfo objects to conveniently analyse the results in Python. See metainfo for more details on how to use the metainfo in Python. + +```python +import sys +from nomad.client import parse, normalize_all + +# match and run the parser +archive = parse(sys.argv[1]) +# run all normalizers +normalize_all(archive) + +# get the 'main section' section_run as a metainfo object +section_run = archive.run[0] + +# get the same data as JSON serializable Python dict +python_dict = section_run.m_to_dict() +``` + + +You can also clone a parser project and use this to debug or fix a parser: +```sh +git clone https://github.com/nomad-coe/nomad-parser-vasp.git +cd nomad-parser-vasp +git checkout metainfo-refactor +python -m nomad.cli nomad parser --show-archive <path-to-your-vasp-file> +``` + +Our parsers are hosted in github. They are in the [nomad-coe](https://github.com/nomad-coe) organization. They are typically named `nomad-parser-<code-name>`. The parser version +that fits the NOMAD v1 metainfo schema is typically in the `metainfo-refactor` branch. +Run the CLI with `python -m nomad.cli` to automatically include the current working directory +in the Python path. This will use the cloned parser code over the installed parser code. \ No newline at end of file diff --git a/examples/archive/archive_query.py b/examples/archive/archive_query.py index 6c651688489efffbd9c27a1a6de09b15e414dc05..dea765eed240d8ed2400d09576e01c5478def17a 100644 --- a/examples/archive/archive_query.py +++ b/examples/archive/archive_query.py @@ -3,24 +3,33 @@ A simple example used in the NOMAD webinar API tutorial ''' from nomad.client import ArchiveQuery +from nomad.metainfo import units query = ArchiveQuery( - url='http://nomad-lab.eu/prod/rae/api', query={ - 'dft.code_name': 'VASP', - 'atoms': ['Ti', 'O'] + 'results.method.simulation.program_name': 'VASP', + 'results.material.elements': ['Ti', 'O'], + 'results.method.simulation.geometry_optimization': { + 'convergence_tolerance_energy_difference:lt': 1e-22, + } }, required={ - 'section_run': { - 'section_single_configuration_calculation[-1]': { - 'energy_total': '*', - 'section_dos': '*' + 'workflow': { + 'calculation_result_ref': { + 'energy': '*', + 'system_ref': { + 'chemical_composition_reduced': '*' + } } } }, - parallel=1, - max=10) + parallel=10, + max=100) print(query) -result = query[0] -print(result.section_run[0].section_single_configuration_calculation[-1].section_dos[0].dos_energies) + +for result in query: + calc = result.workflow[0].calculation_result_ref + formula = calc.system_ref.chemical_composition_reduced + total_energy = calc.energy.total.value.to(units.eV) + print(f'{formula}: {total_energy}') diff --git a/examples/archive/authenticated.py b/examples/archive/authenticated.py new file mode 100644 index 0000000000000000000000000000000000000000..ee9d5966b168689fb1a382f063893c40efa94775 --- /dev/null +++ b/examples/archive/authenticated.py @@ -0,0 +1,16 @@ +''' +A simple example used in the NOMAD webinar API tutorial +''' + +from nomad.client import ArchiveQuery, Auth + +query = ArchiveQuery( + owner='user', + required={ + 'run': { + 'system[-1]': '*' + } + }, + authentication=Auth(user='yourusername', password='yourpassword')) + +print(query) diff --git a/examples/archive/client.ipynb b/examples/archive/client.ipynb deleted file mode 100644 index 5a1a3d29aaa9ba7c2fc3074a7a3188d1b5300e72..0000000000000000000000000000000000000000 --- a/examples/archive/client.ipynb +++ /dev/null @@ -1,248 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:49.442840Z", - "start_time": "2020-04-07T16:02:49.414197Z" - } - }, - "outputs": [], - "source": [ - "from nomad import config\n", - "# this will not be necessary, once this is the official NOMAD version\n", - "config.client.url = 'http://labdev-nomad.esc.rzg.mpg.de/fairdi/nomad/testing-major/api'" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:51.148955Z", - "start_time": "2020-04-07T16:02:49.788395Z" - } - }, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from nomad.client import query_archive\n", - "from nomad.metainfo import units" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:55.371271Z", - "start_time": "2020-04-07T16:02:51.150679Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "total 2398\n", - "-4.506215193227115e-18 joule\n", - "-5.8515279132483456e-18 joule\n", - "-4.251737530685669e-18 joule\n", - "-3.2217721980540896e-18 joule\n", - "-4.437416946700958e-18 joule\n", - "Number queries entries: 2398\n", - "Number of entries loaded in the last api call: 100\n", - "Bytes loaded in the last api call: 34531\n", - "Bytes loaded from this query: 345235\n", - "Number of downloaded entries: 1000\n", - "Number of made api calls: 10\n", - "\n" - ] - } - ], - "source": [ - "aq = query_archive(\n", - " query={\n", - " 'upload_id': ['b5rGMO6dT4Gzqn3JaLjPpw']\n", - " },\n", - " required={\n", - " 'section_run': {\n", - " 'section_single_configuration_calculation[0]': {\n", - " 'energy_total': '*'\n", - " } \n", - " }\n", - " }, \n", - " per_page=100, max=1000)\n", - "\n", - "print('total', aq.total)\n", - "\n", - "for i, e in enumerate(aq):\n", - " if i % 200 == 0:\n", - " print(e.section_run[0].section_single_configuration_calculation[0].energy_total)\n", - " \n", - "print(aq)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:55.379162Z", - "start_time": "2020-04-07T16:02:55.373328Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'section_run': [{'section_single_configuration_calculation': [{'energy_total': -4.506215193227115e-18}]}]}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aq[0].m_to_dict()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:56.680987Z", - "start_time": "2020-04-07T16:02:55.381150Z" - } - }, - "outputs": [], - "source": [ - "aq = query_archive(\n", - " query={\n", - " 'dft.quantities': ['section_dos']\n", - " },\n", - " per_page=1)\n", - "entry = aq[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:56.686165Z", - "start_time": "2020-04-07T16:02:56.682662Z" - } - }, - "outputs": [], - "source": [ - "run = entry.section_run[0]\n", - "dos = next(\n", - " scc.section_dos[0] \n", - " for scc in run.section_single_configuration_calculation\n", - " if len(scc.section_dos) > 0)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:56.927561Z", - "start_time": "2020-04-07T16:02:56.687981Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[<matplotlib.lines.Line2D at 0x1157d5898>]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "<Figure size 432x288 with 1 Axes>" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(dos.dos_energies.to(units.hartree).m, dos.dos_values[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-07T16:02:56.933874Z", - "start_time": "2020-04-07T16:02:56.929657Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'Al6Si40Sr8'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "next(system for system in run.section_system if system.is_representative).chemical_composition_bulk_reduced" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "file_extension": ".py", - "kernelspec": { - "display_name": "Python 3.6.3 64-bit ('.pyenv': virtualenv)", - "language": "python", - "name": "python36364bitpyenvvirtualenv11a6404af23a4e178b049a429667c260" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - }, - "mimetype": "text/x-python", - "name": "python", - "npconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": 3 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/archive/client.py b/examples/archive/client.py deleted file mode 100644 index 3241b4f0972d45aab5feb41d2ee8edcdd5a2ece0..0000000000000000000000000000000000000000 --- a/examples/archive/client.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -A simple example that uses the NOMAD client library to access the archive. -''' - -from nomad.client import ArchiveQuery -from nomad.metainfo import units - - -query = ArchiveQuery( - # url='http://nomad-lab.eu/prod/rae/beta/api', - query={ - 'dft.code_name': 'VASP' - }, - required={ - 'section_run': { - 'section_single_configuration_calculation': '*', - 'section_system': '*' - } - }, - per_page=10, - max=100) - -print(query) - -for i, result in enumerate(query): - if i < 10: - calc = result.section_run[0].section_single_configuration_calculation[-1] - energy = calc.energy_total - formula = calc.single_configuration_calculation_to_system_ref.chemical_composition_reduced - print('%s: energy %s' % (formula, energy.to(units.hartree))) diff --git a/examples/archive/client_workflows.py b/examples/archive/client_workflows.py deleted file mode 100644 index ac6fe94843c6a8daf309db5cfad8cd1381c30d9f..0000000000000000000000000000000000000000 --- a/examples/archive/client_workflows.py +++ /dev/null @@ -1,38 +0,0 @@ -''' -A simple example that uses the NOMAD client library to access the archive. -''' - -from nomad.client import ArchiveQuery -from nomad.metainfo import units - - -query = ArchiveQuery( - query={ - '$and': [ - {'dft.code_name': 'VASP'}, - {'$gte': {'n_atoms': 3}}, - {'$lte': {'dft.workflow.section_geometry_optimization.final_energy_difference': 1e-24}} - ] - }, - required={ - 'section_workflow': { - 'calculation_result_ref': { - 'energy_total': '*', - 'single_configuration_calculation_to_system_ref': { - 'chemical_composition_reduced': '*' - } - } - } - }, - parallel=10, - per_page=10, - max=100) - -for i, result in enumerate(query): - if i < 10: - calc = result.section_workflow.calculation_result_ref - energy = calc.energy_total - formula = calc.single_configuration_calculation_to_system_ref.chemical_composition_reduced - print('%s: energy %s' % (formula, energy.to(units.hartree))) - -print(query) diff --git a/examples/parse.py b/examples/parse.py index a3516fd9684a1795bf4583f08fa7ca3c65b122df..7f194146324698b0d8d44b46ff30685db386045b 100644 --- a/examples/parse.py +++ b/examples/parse.py @@ -7,7 +7,7 @@ archive = parse(sys.argv[1]) normalize_all(archive) # get the 'main section' section_run as a metainfo object -section_run = archive.section_run[0] +section_run = archive.run[0] # get the same data as JSON serializable Python dict python_dict = section_run.m_to_dict() diff --git a/mkdocs.yml b/mkdocs.yml index 0b6f72f5922e8d61bd48e7198322abcfbf2fdb51..b2fd8274e6842a905b0547b6044bcb86309b03bd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,8 +6,8 @@ nav: - Introduction: index.md - web.md - api.md - - Using the AI Toolkit and other remote tools: aitoolkit.md - - Using the Python library: pythonlib.md + - pythonlib.md + # - Using the AI Toolkit and other remote tools: aitoolkit.md - Extending and Developing NOMAD: - developers.md - metainfo.md diff --git a/nomad/client/archive.py b/nomad/client/archive.py index 4655a05469a75b04a0829ab2f36d1ea2b0c2f830..d899ceb7204e9ff678a4ed24e6562922a13b6dd6 100644 --- a/nomad/client/archive.py +++ b/nomad/client/archive.py @@ -17,117 +17,10 @@ # ''' -Access the NOMAD archive with the NOMAD client library -______________________________________________________ - Retrieve and analyse (large amounts) of NOMAD's archive data. This allows to use queries to filter for desired entries, bulk download the required parts of the respective archives, and navigate the results using NOMAD's metainfo Python API. - -.. literalinclude:: ../examples/archive/client.py - :language: python - -This script should yield a result like this: - -.. code:: - - Number queries entries: 7628 - Number of entries loaded in the last api call: 10 - Bytes loaded in the last api call: 118048 - Bytes loaded from this query: 118048 - Number of downloaded entries: 10 - Number of made api calls: 1 - - Cd2O2: energy -11467.827149010665 hartree - Sr2O2: energy -6551.45699684026 hartree - Sr2O2: energy -6551.461104765451 hartree - Be2O2: energy -178.6990610734937 hartree - Ca2O2: energy -1510.3938165430286 hartree - Ca2O2: energy -1510.3937761449583 hartree - Ba2O2: energy -16684.667362890417 hartree - Mg2O2: energy -548.9736595672932 hartree - Mg2O2: energy -548.9724185656775 hartree - Ca2O2: energy -1510.3908614326358 hartree - -Let's discuss the different elements here. First, we have a set of imports. The NOMAD source -codes comes with various sub-modules. The `client` module contains everything related -to what is described here; the `metainfo` is the Python interface to NOMAD's common -archive data format and its data type definitions; the `config` module simply contains -configuration values (like the URL to the NOMAD API). - -Next, we create an :class:`ArchiveQuery` instance. This object will be responsible for talking -to NOMAD's API for us in a transparent and lazy manner. This means, it will not download -all data right away, but do so when we are actually iterating through the results. - -The archive query takes several parameters: - -- The ``query`` is a dictionary of search criteria. The query is used to filter all of NOMAD's - entry down to a set of desired entries. You can use NOMAD's GUI to create queries and - copy their Python equivalent with the ``<>``-code button on the result list. -- The ``required`` part, allows to specify what parts of the archive should be downloaded. - Leave it out to download the whole archives. Based on NOMAD's Metainfo (the 'schema' of - all archives), you can determine what sections to include and which to leave out. Here, - we are interested in the first run (usually entries only have one run) and the first - calculation result. -- With the optional ``per_page`` you can determine, how many results are downloaded at - a time. For bulk downloading many results, we recommend ~100. If you are just interested - in the first results a lower number might increase performance. -- With the optional ``max``, we limit the maximum amount of entries that are downloaded, - just to avoid accidentely iterating through a result set of unknown and potentially large - size. - -When you print the archive query object, you will get some basic statistics about the -query and downloaded data. - -The archive query object can be treated as a Python list-like. You use indices and ranges -to select results. Here we iterate through a slice and print the calculated energies -from the first calculation of the entries. Each result is a Python object with attributes -governed by the NOMAD Metainfo. Quantities yield numbers, string, or numpy arrays, while -sub-sections return lists of further objects. Here we navigate the sections ``run`` and -sub-section ``energy`` and sub-section ``total`` to access the quantity ``value``. This quantity is a -number with an attached unit (Joule), which can be converted to something else (e.g. Hartree). - -The create query object keeps all results in memory. Keep this in mind, when you are -accessing a large amount of query results. You should use :func:`ArchiveQuery.clear` -to remove unnecessary results. - -The NOMAD Metainfo -__________________ - -You can imagine the NOMAD Metainfo as a complex schema for hiearchically organized scientific -data. In this sense, the NOMAD Metainfo is a set of data type definitions. These definitions -then govern how the archive for an data entry in NOMAD might look like. You can browse the -hierarchy of definitions in our `Metainfo browser <../metainfo>`_. - -Be aware, that the definitions entail everything that an entry could possibly contain, but -not all entries contain all sections and all quantities. What an entry contains depends -on the information that the respective uploaded data contained, what could be extracted, -and of course what was calculated in the first place. To see what the archive of an concrete -entry looks like, you can use the `search interface <../search>`_, select an entry from the -list fo search results, and click on the *Archive* tab. - -To *see inside* an archive object in Python, you can use :func:`nomad.metainfo.MSection.m_to_dict` -which is provided by all archive objects. This will convert a (part of an) archive into a -regular, JSON-serializable Python dictionary. - -For more details on the metainfo Python interface, consult the `metainfo documentation <metainfo.html>`_. - -The ArchiveQuery class -______________________ - -.. autoclass:: ArchiveQuery - -Working with private data -_________________________ - -Public NOMAD data can be accessed without any authentication; everyone can use our API -without the need for an account or login. However, if you want to work with your own -data that is not yet published, or embargoed data was shared with you, you need to -authenticate before accessing this data. Otherwise, you will simply not find it with -your queries. To authenticate simply provide your NOMAD username and password to the -:class:`ArchiveQuery` constructor. - ''' from typing import Dict, Any, List @@ -136,6 +29,7 @@ import requests from io import StringIO import math import multiprocessing +import json from nomad import config from nomad import metainfo as mi @@ -152,6 +46,9 @@ class QueryError(Exception): class ApiStatistics(mi.MSection): + total = mi.Quantity( + type=int, default=0, + description='Total number of entries that fulfil the query') nentries = mi.Quantity( type=int, default=0, @@ -194,7 +91,7 @@ class ProcState: self.url = archive_query.url self.query_and_list = archive_query.query_and_list self.request: Dict[str, Any] = dict( - owner='visible', + owner=archive_query.owner, required=archive_query.required) self.per_page = archive_query.per_page self.authentication = archive_query.authentication @@ -279,12 +176,13 @@ class ArchiveQuery(collections.abc.Sequence): call. ''' def __init__( - self, + self, owner: str = 'visible', query: dict = None, required: dict = None, url: str = None, username: str = None, password: str = None, parallel: int = 1, per_page: int = 10, max: int = 10000, - authentication: Auth = None): + authentication: Auth = None, auth: Auth = None): + self.owner = owner self.page = 1 self.parallel = parallel self.per_page = per_page @@ -300,32 +198,57 @@ class ArchiveQuery(collections.abc.Sequence): # results with those properties are returned. quantities = set() - def collect(required, parent_def_name: str = None): + def collect(required, parent_section: mi.Section, parent_path: str = None): if not isinstance(required, dict): return for key, value in required.items(): def_name = key.split('[')[0] - qualified_def_name = def_name - if parent_def_name: - qualified_def_name = f'{parent_def_name}.{def_name}' + definition = parent_section.all_properties.get(def_name) + if definition is None: + raise KeyError(f'{def_name} is not a property of {parent_section}') + + if parent_path: + qualified_def_name = f'{parent_path}.{def_name}' + else: + qualified_def_name = def_name quantities.add(qualified_def_name) - collect(value, qualified_def_name) - collect(required) + if isinstance(definition, mi.SubSection): + collect( + value, + parent_section=definition.section_def, + parent_path=qualified_def_name) + elif isinstance(definition, mi.Quantity) and isinstance(definition.type, mi.Reference): + next_parent_section = definition.type.target_section_def.m_resolved() + parent_path = next_parent_section.path + if parent_path in ['__ambiguous__', '__no_archive_path__']: + continue + collect( + value, + parent_section=next_parent_section, + parent_path=parent_path) + + collect(required, parent_section=EntryArchive.m_def) self.query_and_list.append({'quantities': list(quantities)}) self.password = password self.username = username self.url = config.client.url if url is None else url self._authentication = authentication + if not self._authentication: + self._authentication = auth self._total = -1 self._results: List[dict] = [] self._statistics = ApiStatistics() self._proc_states: List[ProcState] = None + @property + def query(self): + return {'and': self.query_and_list} + @property def authentication(self): ''' @@ -352,10 +275,8 @@ class ArchiveQuery(collections.abc.Sequence): while True: uploads_request = { - 'owner': 'visible', - 'query': { - 'and': self.query_and_list - }, + 'owner': self.owner, + 'query': self.query, 'pagination': { 'page_size': 0 }, @@ -382,6 +303,7 @@ class ArchiveQuery(collections.abc.Sequence): raise Exception( 'Error requesting NOMAD API: HTTP %d' % response.status_code) + total = response_json['pagination']['total'] agg_data = response_json['aggregations']['uploads']['terms'] after = agg_data['pagination'].get('next_page_after_value', None) values = {bucket['value']: bucket for bucket in agg_data['data']} @@ -420,6 +342,7 @@ class ArchiveQuery(collections.abc.Sequence): self._proc_states.append(proc_state) self._total = nentries + self._statistics.total = total self._statistics.nentries = nentries def call_api(self): @@ -475,7 +398,7 @@ class ArchiveQuery(collections.abc.Sequence): if self._total == -1: self.call_api() - return str(self._statistics) + return f'Query: {json.dumps(self.query, indent=2)}\n{self._statistics}' def __getitem__(self, key): if isinstance(key, slice): diff --git a/nomad/datamodel/datamodel.py b/nomad/datamodel/datamodel.py index 76299f7c391ec37e1757d83373fa5ece2db85b2c..df3ee99c566963fc16aabd28c4f9029b6fa4945a 100644 --- a/nomad/datamodel/datamodel.py +++ b/nomad/datamodel/datamodel.py @@ -701,3 +701,6 @@ class EntryArchive(metainfo.MSection): sub_section=Results, categories=[FastAccess], a_elasticsearch=Elasticsearch(auto_include_subsections=True)) + + +m_package.__init_metainfo__() diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index dfd0b51675b052a59494394d538ae4569f5d1598..f06e352551bbcbb079ebf7135cb64f1ff6dd6a13 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -2637,6 +2637,8 @@ class SubSection(Property): times in the parent section. ''' + _used_sections: Dict['Section', Set['SubSection']] = {} + sub_section: 'Quantity' = _placeholder_quantity repeats: 'Quantity' = _placeholder_quantity @@ -2824,6 +2826,9 @@ class Section(Definition): A helper attribute that gives all inner_section_definitions including their aliases by name. + path: Shortest path from a root section to this section. This is not the path + in the metainfo schema (`m_path`) but a archive path in potential data. + event_handlers: Event handler are functions that get called when the section data is changed. There are two types of events: ``set`` and ``add_sub_section``. The handler type @@ -2859,6 +2864,7 @@ class Section(Definition): all_sub_sections_by_section: 'Quantity' = _placeholder_quantity all_aliases: 'Quantity' = _placeholder_quantity all_inner_section_definitions: 'Quantity' = _placeholder_quantity + path: 'Quantity' = _placeholder_quantity def __init__(self, *args, validate: bool = True, **kwargs): self._section_cls: Type[MSection] = None @@ -3016,8 +3022,10 @@ class Package(Definition): if isinstance(content.type, MProxy): content.type.m_proxy_resolve() elif isinstance(content, SubSection): - if isinstance(content.sub_section, MProxy): - content.sub_section.m_proxy_resolve() + target = content.sub_section + if isinstance(target, MProxy): + target = target.m_proxy_resolve() + SubSection._used_sections.setdefault(target, []).append(content) elif isinstance(content, Section): for base_section in content.base_sections: if isinstance(base_section, MProxy): @@ -3227,6 +3235,30 @@ def all_inner_section_definitions(self) -> Dict[str, Section]: return result +@derived(cached=True) +def section_path(self) -> str: + used_in_sub_sections: List[SubSection] = SubSection._used_sections.get(self, []) # type: ignore + if len(used_in_sub_sections) == 0: + if self.name == 'EntryArchive': + return None + else: + return '__no_archive_path__' + + if len(used_in_sub_sections) > 1: + return '__ambiguous__' + + parent_section = used_in_sub_sections[0].m_parent + parent_path = parent_section.path + + if parent_path is None: + return used_in_sub_sections[0].name + + if parent_path.startswith('__'): + return parent_path + + return f'{parent_path}.{used_in_sub_sections[0].name}' + + Section.inherited_sections = inherited_sections Section.all_base_sections = all_base_sections Section.all_properties = all_properties @@ -3235,6 +3267,8 @@ Section.all_sub_sections = all_sub_sections Section.all_sub_sections_by_section = all_sub_sections_by_section Section.all_aliases = all_aliases Section.all_inner_section_definitions = all_inner_section_definitions +Section.path = section_path + Property.template = Quantity(type=bool, name='template', default=False) diff --git a/nomad/mkdocs.py b/nomad/mkdocs.py index a0082d1c20dbfc09d7c2de1a1c4f49b13e335b37..19fa2d2eef08b5c8bf55d66ea9e91378a8a571df 100644 --- a/nomad/mkdocs.py +++ b/nomad/mkdocs.py @@ -24,7 +24,7 @@ from nomad.app.v1.models import ( query_documentation, owner_documentation) from nomad.app.v1.routers.entries import archive_required_documentation -from nomad import config +from nomad import config, utils doc_snippets = { 'query': query_documentation, @@ -41,3 +41,10 @@ def define_env(env): @env.macro def doc_snippet(key): # pylint: disable=unused-variable return doc_snippets[key] + + @env.macro + def metainfo_data(): # pylint: disable=unused-variable + return utils.strip(''' + You can browse the [NOMAD metainfo schema]({{ nomad_url() }}/../gui/analyze/metainfo) + or the archive of each entry (e.g. [a VASP example]({{ nomad_url() }}/../gui/search/entries/entry/id/d5OYC0SJTDevHMPk7YHd4A/-7j8ojKkna2NLXdytv_OjV4zsBXw/archive)) + in the web-interface.''') diff --git a/tests/metainfo/test_sections.py b/tests/metainfo/test_sections.py index caaa18b6b45405e1719921597071a6c6a15d76d2..d8482c50ec559ef6843298930839fbc260493d58 100644 --- a/tests/metainfo/test_sections.py +++ b/tests/metainfo/test_sections.py @@ -22,7 +22,7 @@ import pytest from nomad.metainfo import MSection -from nomad.metainfo.metainfo import Quantity, SubSection, Section +from nomad.metainfo.metainfo import Package, Quantity, SubSection, Section def test_base_section(): @@ -193,3 +193,26 @@ def test_inner_sections_inheritance(): section = OuterSection( test_sub_section=OuterSection.InnerSection(test_quantity='test_value')) assert section.test_sub_section.test_quantity == 'test_value' + + +def test_path(): + class ChildSection(MSection): + pass + + class EntryArchive(MSection): + child = SubSection(sub_section=ChildSection.m_def) + + pkg = Package() + pkg.section_definitions.append(ChildSection.m_def) + pkg.section_definitions.append(EntryArchive.m_def) + pkg.__init_metainfo__() + + assert SubSection._used_sections[ChildSection.m_def] == [EntryArchive.child] + assert ChildSection.m_def.path == 'child' + + from nomad.datamodel.metainfo.simulation.calculation import Calculation, Energy + from nomad.datamodel.metainfo.simulation.system import System + from nomad.datamodel import EntryArchive # pylint: disable=unused-import + assert Calculation.m_def.path == 'run.calculation' + assert System.m_def.path == 'run.system' + assert Energy.m_def.path == '__no_archive_path__'