diff --git a/aiida/orm/nodes/data/array/array.py b/aiida/orm/nodes/data/array/array.py
index 166eca8e53..89cb8c5357 100644
--- a/aiida/orm/nodes/data/array/array.py
+++ b/aiida/orm/nodes/data/array/array.py
@@ -201,9 +201,11 @@ def _get_array_entries(self):
the value is the numpy array transformed into a list. This is so that
it can be transformed into a json object.
"""
+
array_dict = {}
for key, val in self.get_iterarrays():
- array_dict[key] = val.tolist()
+
+ array_dict[key] = clean_array(val)
return array_dict
def _prepare_json(self, main_file_name='', comments=True): # pylint: disable=unused-argument
@@ -222,3 +224,28 @@ def _prepare_json(self, main_file_name='', comments=True): # pylint: disable=un
json_dict['comments'] = get_file_header(comment_char='')
return json.dumps(json_dict).encode('utf-8'), {}
+
+
+def clean_array(array):
+ """
+ Replacing np.nan and np.inf/-np.inf for Nones.
+
+ The function will also sanitize the array removing ``np.nan`` and ``np.inf``
+ for ``None`` of this way the resulting JSON is always valid.
+ Both ``np.nan`` and ``np.inf``/``-np.inf`` are set to None to be in
+ accordance with the
+ `ECMA-262 standard `_.
+
+ :param array: input array to be cleaned
+ :return: cleaned list to be serialized
+ :rtype: list
+ """
+ import numpy as np
+
+ output = np.reshape(
+ np.asarray([
+ entry if not np.isnan(entry) and not np.isinf(entry) else None for entry in array.flatten().tolist()
+ ]), array.shape
+ )
+
+ return output.tolist()
diff --git a/docs/source/howto/share_data.rst b/docs/source/howto/share_data.rst
index 1a5314688c..7ffa7e4faa 100644
--- a/docs/source/howto/share_data.rst
+++ b/docs/source/howto/share_data.rst
@@ -183,6 +183,11 @@ The AiiDA REST API allows to query your AiiDA database over HTTP(S) and returns
As of October 2020, the AiiDA REST API only supports ``GET`` methods (reading); in particular, it does *not* yet support workflow management.
This feature is, however, part of the `AiiDA roadmap `_.
+
+.. note::
+ To ensure that when serving ``orm.ArrayData`` one always obtains a valid JSON compliant with the `ECMA-262 standard `_, any ``np.nan``, ``np.inf`` and/or ``-np.inf`` entries will be replaced by ``None`` which will be rendered as ``null`` when getting the array via the API call.
+
+
.. _how-to:share:serve:launch:
Launching the REST API
diff --git a/docs/source/reference/rest_api.rst b/docs/source/reference/rest_api.rst
index 6bd21e5983..b4fdb56d7d 100644
--- a/docs/source/reference/rest_api.rst
+++ b/docs/source/reference/rest_api.rst
@@ -946,7 +946,7 @@ Querybuilder
Posts a query to the database. The content of the query is passed in a attached JSON file.
To use this endpoint, you need a http operator that allows to pass attachments.
-We will demonstrate two options, the `HTTPie `_ (to use in the terminal) and the python library `Requests `_ (to use in python).
+We will demonstrate two options, the `HTTPie `_ (to use in the terminal) and the python library `Requests `_ (to use in python).
Option 1: HTTPie
diff --git a/tests/restapi/test_routes.py b/tests/restapi/test_routes.py
index ec45be3a98..5588970dcd 100644
--- a/tests/restapi/test_routes.py
+++ b/tests/restapi/test_routes.py
@@ -14,11 +14,13 @@
import json
from flask_cors.core import ACL_ORIGIN
+import numpy as np
import pytest
from aiida import orm
from aiida.common.links import LinkType
from aiida.manage import get_manager
+from aiida.orm.nodes.data.array.array import clean_array
from aiida.restapi.run_api import configure_api
@@ -141,6 +143,12 @@ def init_profile(self, aiida_profile_clean, aiida_localhost): # pylint: disable
computer = orm.Computer(**dummy_computer)
computer.store()
+ # Setting array data for the tests
+ array = orm.ArrayData()
+ array.set_array('array_clean', np.asarray([[4, 5, 7], [9, 5, 1], [3, 4, 4]]))
+ array.set_array('array_dirty', np.asarray([[4, 5, np.nan], [9, np.inf, -1 * np.inf], [np.nan, 4, 4]]))
+ array.store()
+
# Prepare typical REST responses
self.process_dummy_data()
@@ -205,6 +213,7 @@ def process_dummy_data(self):
'parameterdata': orm.Dict,
'structuredata': orm.StructureData,
'data': orm.Data,
+ 'arraydata': orm.ArrayData,
}
for label, dataclass in data_types.items():
data = orm.QueryBuilder().append(dataclass, tag='data', project=data_projections).order_by({
@@ -300,7 +309,6 @@ def process_test(
if result_node_type is None and result_name is None:
result_node_type = entity_type
result_name = entity_type
-
url = self._url_prefix + url
with self.app.test_client() as client:
@@ -321,7 +329,6 @@ def process_test(
else:
from aiida.common.exceptions import InputValidationError
raise InputValidationError('Pass the expected range of the dummydata')
-
expected_node_uuids = [node['uuid'] for node in expected_data]
result_node_uuids = [node['uuid'] for node in response['data'][result_name]]
assert expected_node_uuids == result_node_uuids
@@ -745,7 +752,7 @@ def test_calculation_inputs(self):
self.process_test(
'nodes',
f'/nodes/{str(node_uuid)}/links/incoming?orderby=id',
- expected_list_ids=[5, 3],
+ expected_list_ids=[6, 4],
uuid=node_uuid,
result_node_type='data',
result_name='incoming'
@@ -759,7 +766,7 @@ def test_calculation_input_filters(self):
self.process_test(
'nodes',
f"/nodes/{str(node_uuid)}/links/incoming?node_type=\"data.core.dict.Dict.\"",
- expected_list_ids=[3],
+ expected_list_ids=[4],
uuid=node_uuid,
result_node_type='data',
result_name='incoming'
@@ -1347,3 +1354,26 @@ def test_querybuilder_project_implicit(self):
# All are Nodes, and all properties are projected, full_type should be present
assert 'full_type' in entity
assert 'attributes' in entity
+
+ def test_array_download(self):
+ """
+ Test download of arraydata as a json file
+ """
+ from aiida.orm import load_node
+
+ node_uuid = self.get_dummy_data()['arraydata'][0]['uuid']
+ url = f'{self.get_url_prefix()}/nodes/{node_uuid}/download?download_format=json&download=False'
+ with self.app.test_client() as client:
+ rv_obj = client.get(url)
+
+ data_json = json.loads(rv_obj.json['data']['download']['data'])
+
+ assert json.dumps(data_json, allow_nan=False)
+
+ data_array = load_node(node_uuid)
+ array_names = data_array.get_arraynames()
+ for name in array_names:
+ if not np.isnan(data_array.get_array(name)).any():
+ assert np.allclose(data_array.get_array(name), data_json[name])
+ else:
+ assert clean_array(data_array.get_array(name)) == data_json[name]