Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit a6444dc

Browse files
authored
Merge pull request #247 from raphaelrpl/b-0.8
WIP: ✅ review unittests for cube creation
2 parents 7e24a6a + 1ee5da8 commit a6444dc

8 files changed

Lines changed: 144 additions & 10 deletions

File tree

CHANGES.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ Changes
2121
=======
2222

2323

24-
Version 0.8.4
25-
-------------
24+
Version 0.8.4 (2023-01-23)
25+
--------------------------
2626

2727
- Add support to generate data cube from Landsat Collection 2 (`#172 <https://github.com/brazil-data-cube/cube-builder/issues/172>`_)
2828
- Add support to combine Landsat Collection 2 sensors (L5/L7, L7/L8, L7/L8/L9) using single collection (`#172 <https://github.com/brazil-data-cube/cube-builder/issues/172>`_)
2929
- Review API error when no parameter is set.
30+
- Review unittests for cube creation and code coverage.
3031

3132

3233
Version 0.8.3 (2022-10-03)

cube_builder/alembic/f639d83487b6_v0_4_0.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
# revision identifiers, used by Alembic.
3232
revision = 'f639d83487b6'
33-
down_revision = None
33+
down_revision = 'c68b17b1860b'
3434
branch_labels = ('cube_builder',)
3535
depends_on = None
3636

cube_builder/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def get_cube(cls, cube_id: int):
344344
@classmethod
345345
def list_cubes(cls):
346346
"""Retrieve the list of data cubes from Brazil Data Cube database."""
347-
cubes = Collection.query().filter(Collection.collection_type == 'cube').all()
347+
cubes = Collection.query().filter(Collection.collection_type == 'cube').order_by(Collection.id).all()
348348

349349
serializer = CollectionForm()
350350

cube_builder/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class CustomMaskDefinition(Schema):
105105
nodata = fields.Integer(required=True, allow_none=False)
106106
saturated_data = fields.List(fields.Integer, required=False, allow_none=False)
107107
saturated_band = fields.String(required=False, allow_none=False)
108-
bits = fields.Boolean(required=False, allow_none=False, default=False)
108+
bits = fields.Boolean(required=False, allow_none=False, dump_default=False)
109109

110110

111111
class CubeParametersSchema(Schema):

cube_builder/utils/image.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import os
2323
from concurrent.futures import ThreadPoolExecutor
2424
from pathlib import Path
25-
from typing import Iterable, List, Optional, Union, Tuple
25+
from typing import Iterable, List, Optional, Tuple, Union
2626
from urllib.parse import urlparse
2727

2828
import numpy
@@ -401,7 +401,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
401401

402402
def close(self):
403403
"""Close rasterio data set."""
404-
if not self.dataset.closed:
404+
if hasattr(self, 'dataset') and not self.dataset.closed:
405405
logging.debug('Closing dataset {}'.format(str(self.path)))
406406

407407
if self.mode == 'w' and self.tags:
@@ -613,9 +613,14 @@ def rescale(array: ArrayType, multiplier: float, new_scale: float,
613613
Tip:
614614
When dealing with negative ``origin_additive`` factor or values which may be negative,
615615
make sure to use right numpy dtype and
616-
`Numpy Masked Arrays <https://numpy.org/doc/stable/reference/maskedarray.html>`_
616+
`Numpy Masked Arrays`_
617617
to mask ``nodata`` values to avoid value limit coercion.
618618
619+
Note:
620+
When the result value overflow the data type,
621+
the value is coerced to the data type limits.
622+
See more in `numpy.iinfo <https://numpy.org/doc/stable/reference/generated/numpy.iinfo.html>`_.
623+
619624
Args:
620625
array: Input array
621626
multiplier: Origin array scale multiplier
@@ -626,6 +631,10 @@ def rescale(array: ArrayType, multiplier: float, new_scale: float,
626631
Examples:
627632
This example covers the rescaling Landsat Collection 2 arrays
628633
(1-65535, scale=0.0000275 - 0.2) into 0-10000 values.
634+
635+
.. doctest::
636+
:skipif: True
637+
629638
>>> import numpy
630639
>>> from cube_builder.utils.image import rescale
631640
>>> arr3d = numpy.random.randint(1, 65535, (3, 3), dtype=numpy.uint16)

cube_builder/utils/processing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
TOTAL_OBSERVATION_NAME)
5151
# Builder
5252
from . import get_srid_column
53-
from .image import (SmartDataSet, generate_cogs, linear_raster_scale, raster_convexhull,
54-
raster_extent, rescale, save_as_cog)
53+
from .image import (SmartDataSet, generate_cogs, linear_raster_scale, raster_convexhull, raster_extent, rescale,
54+
save_as_cog)
5555
from .index_generator import generate_band_indexes
5656
from .strings import StringFormatter
5757

tests/test_api.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020

2121
import datetime
2222

23+
from click.testing import CliRunner
2324
from dateutil.relativedelta import relativedelta
2425
from flask import Response
2526

27+
from cube_builder import __version__
28+
from cube_builder.cli import cli
29+
2630

2731
def _assert_json_request(response: Response, status_code=200):
2832
assert response.status_code == status_code
@@ -31,6 +35,12 @@ def _assert_json_request(response: Response, status_code=200):
3135
return response.json
3236

3337

38+
def test_index_api(client):
39+
response = client.get('/')
40+
assert response.status_code == 200
41+
assert response.json['description'] == 'Cube Builder' and response.json['version'] == __version__
42+
43+
3444
def test_create_grid(client, json_data):
3545
response = client.post('/create-grids', json=json_data['grid-bdc-md.json'])
3646
assert response.status_code == 201
@@ -43,6 +53,11 @@ def test_get_grid(client, json_data):
4353
assert len(grids) > 0
4454

4555

56+
def test_load_initial_data():
57+
res = CliRunner().invoke(cli, args=['load-data'])
58+
assert res.exit_code == 0
59+
60+
4661
def test_create_cube(client, json_data):
4762
json_cube = json_data['lc8-16d-stk.json']
4863
response = client.post('/cubes', json=json_cube)
@@ -81,3 +96,57 @@ def test_list_periods_continuous_month(client):
8196
assert end == (ref_date + offset)
8297

8398
ref_date += offset + relativedelta(days=1)
99+
100+
101+
def test_datacube_status(client, json_data):
102+
json_cube = json_data['lc8-16d-stk.json']
103+
identifier = f"{json_cube['datacube']}-{json_cube['version']}"
104+
response = client.get('/cube-status', query_string={'cube_name': identifier})
105+
_assert_json_request(response, status_code=200)
106+
107+
# Test invalid request
108+
response = client.get('/cube-status')
109+
_assert_json_request(response, status_code=400)
110+
111+
112+
def test_list_cubes(client):
113+
cube_info = _get_first_cube(client)
114+
115+
response = client.get(f'/cubes/{cube_info["id"]}')
116+
cube = _assert_json_request(response, 200)
117+
assert cube['name'] == cube_info['name']
118+
119+
120+
def test_update_cube_meta(client):
121+
cube = _get_first_cube(client)
122+
123+
props = dict(
124+
title="New Cube - Updated",
125+
public=True
126+
)
127+
response = client.put(f'/cubes/{cube["id"]}', json=props)
128+
res = _assert_json_request(response, 200)
129+
assert res['message'] == 'Updated cube!'
130+
updated_cube = _get_first_cube(client)
131+
132+
assert updated_cube['title'] == props['title']
133+
assert updated_cube['is_public'] == props['public']
134+
135+
# invalid parameter
136+
props['public'] = 'invalid'
137+
response = client.put(f'/cubes/{cube["id"]}', json=props)
138+
_assert_json_request(response, 400)
139+
140+
141+
def test_list_cube_tiles(client):
142+
cube = _get_first_cube(client)
143+
144+
response = client.get(f'/cubes/{cube["id"]}/tiles')
145+
_assert_json_request(response, 200)
146+
147+
148+
def _get_first_cube(client):
149+
response = client.get('/cubes')
150+
cubes = _assert_json_request(response, 200)
151+
assert len(cubes) > 0
152+
return cubes[0]

tests/test_image.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,58 @@ def test_create_empty_raster():
6767
data = ds.read(1)
6868

6969
assert data.min() == data.max() == nodata
70+
71+
72+
def test_rescale_raster():
73+
"""Test scale values of array into a new range.
74+
75+
This test consists in adapt the values of collections like
76+
Landsat Collection 2 Level 2 transforming
77+
the range 8000~40000 with additive -0.2 into range 0-10000."""
78+
# Landsat C2 L2 values
79+
arr2d = numpy.array([
80+
[17834, 16269, 8275, 27369],
81+
[15755, 19081, 21684, 20865],
82+
[24655, 21416, 8932, 32578],
83+
[27168, 21827, 11382, 37001],
84+
], dtype=numpy.uint16)
85+
86+
res = image.rescale(arr2d, 0.0000275, new_scale=0.0001, origin_additive=-0.2)
87+
88+
assert res.max() < 10000
89+
expected = numpy.array([[2904, 2473, 275, 5526],
90+
[2332, 3247, 3963, 3737],
91+
[4780, 3889, 456, 6958],
92+
[5471, 4002, 1130, 8175]], dtype=numpy.uint16)
93+
assert numpy.array_equal(res, expected)
94+
95+
96+
def test_linear_raster_scale():
97+
"""Test linear raster scaling to transform band values int16 into byte."""
98+
arr2d = numpy.random.randint(0, 10000, (10, 10), dtype=numpy.uint16)
99+
100+
# Cast to uint8
101+
rescaled_arr2d = image.linear_raster_scale(arr2d, (0, arr2d.max(),), (0, 255))
102+
103+
assert 0 <= rescaled_arr2d.min() <= rescaled_arr2d.max()
104+
105+
# Using masked array
106+
arr2d_ma = numpy.ma.array(arr2d, mask=11000)
107+
rescaled_arr2d_ma = image.linear_raster_scale(arr2d_ma, (0, arr2d_ma.max(),), (0, 255))
108+
assert 0 <= rescaled_arr2d.min() <= rescaled_arr2d.max()
109+
110+
111+
def test_bit_extraction():
112+
"""Test bit extraction from any value (used for Landsat sensors)."""
113+
# 43 => 0010 1011
114+
for position in [0, 1, 3, 5]:
115+
bit_pos_value = image.extract_qa_bits(43, position)
116+
expected = 2 ** position
117+
assert bit_pos_value == expected
118+
119+
120+
def test_radsat_bit():
121+
data = numpy.array([1026, 1037], dtype=numpy.uint16)
122+
saturated_values = image.radsat_extract_bits(data, 1, 7)
123+
expected = numpy.array([1, 6], dtype=numpy.uint16)
124+
assert numpy.array_equal(saturated_values, expected)

0 commit comments

Comments
 (0)