Skip to content

Commit cff2ee7

Browse files
Feature/coverage nightlight (#659)
* Add coverage test for nightlight 1) Add tests for functions: - load_nasa_nl_shape_single_tile - load_nightlight_noaa - untar_noaa_stable_nighlight 2) Add raises message if wrong layer is selected in function: load_nasa_nl_shape_single_tile 3 ) Add logger message in function: load_nasa_nl_shape_single_tile 4) Correct test unzip_tif_to_py 5) Apply uniform formatting across all tests of nightlight.py module * Fix pylint errors 1) line too long 2 )use %s for logging instead of f-strings * Complete test for gpw_population function Add small test for function load_gpw_pop_shape() situated in: climada/entity/exposures/litpop/gpw_population.py * Add test for litpop functions Add test for functions: 1) from_nightlight_intensity() 2) from_population() * Add nighlight test Add test for function climada/entity/exposures/litpop/nightlight.py get_required_nl_files() remove unnecessary brackets from nightlight.py * Add test for check_nl_local_file_exists * correct check_nl_local_file_exists 2 BM_FILES in local data folder downloaded with climada but 5 BM_FILES on the system dir of jenkins * correct test
1 parent dfb1a92 commit cff2ee7

File tree

4 files changed

+260
-75
lines changed

4 files changed

+260
-75
lines changed

climada/entity/exposures/litpop/nightlight.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,15 @@ def get_required_nl_files(bounds):
200200

201201
# Now latitude. The height of all tiles is the same as the height.
202202
# Note that for this analysis returns an index which follows from North to South oritentation.
203-
first_tile_lat = min(np.floor(-(min_lat - (90)) / tile_width), 1)
203+
first_tile_lat = min(np.floor(-(min_lat - 90) / tile_width), 1)
204204
last_tile_lat = min(np.floor(-(max_lat - 90) / tile_width), 1)
205205

206206
for i_lon in range(0, int(len(req_files) / 2)):
207207
if first_tile_lon <= i_lon <= last_tile_lon:
208208
if first_tile_lat == 0 or last_tile_lat == 0:
209-
req_files[((i_lon)) * 2] = 1
209+
req_files[(i_lon) * 2] = 1
210210
if first_tile_lat == 1 or last_tile_lat == 1:
211-
req_files[((i_lon)) * 2 + 1] = 1
211+
req_files[(i_lon) * 2 + 1] = 1
212212
else:
213213
continue
214214
return req_files
@@ -355,6 +355,10 @@ def load_nasa_nl_shape_single_tile(geometry, path, layer=0):
355355
with rasterio.open(path, 'r') as src:
356356
# read cropped data from source file (src) to np.ndarray:
357357
out_image, transform = rasterio.mask.mask(src, [geometry], crop=True)
358+
LOGGER.debug('Read cropped %s as np.ndarray.', path.name)
359+
if out_image.shape[0] < layer:
360+
raise IndexError(f"{path.name} has only {out_image.shape[0]} layers,"
361+
f" layer {layer} can't be accessed.")
358362
meta = src.meta
359363
meta.update({"driver": "GTiff",
360364
"height": out_image.shape[1],

climada/entity/exposures/test/test_nightlight.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from climada.entity.exposures.litpop import nightlight
2525
from climada.util.constants import SYSTEM_DIR
26+
from pathlib import Path
2627

2728
BM_FILENAMES = nightlight.BM_FILENAMES
2829

@@ -79,6 +80,75 @@ def test_download_nightlight_files(self):
7980
# The same length but not the correct length
8081
self.assertRaises(ValueError, nightlight.download_nl_files, (1, 0, 1), (1, 1, 1))
8182

83+
def test_get_required_nl_files(self):
84+
""" get_required_nl_files return a boolean matrix of 0 and 1
85+
indicating which tile of NASA nighlight files are needed giving
86+
a bounding box. This test check a few configuration of tiles
87+
and check that a value error is raised if the bounding box are
88+
incorrect """
89+
90+
# incorrect bounds: bounds size =! 4, min lon > max lon, min lat > min lat
91+
BOUNDS = [(20, 30, 40),
92+
(120, -20, 110, 30),
93+
(-120, 50, 130, 10)]
94+
# correct bounds
95+
bounds_c1 = (-120, -20, 0, 40)
96+
bounds_c2 = (-70, -20, 10, 40)
97+
bounds_c3 = (160, 10, 180, 40)
98+
99+
for bounds in BOUNDS:
100+
with self.assertRaises(ValueError) as cm:
101+
102+
nightlight.get_required_nl_files(bounds = bounds)
103+
104+
self.assertEqual('Invalid bounds supplied. `bounds` must be tuple'
105+
' with (min_lon, min_lat, max_lon, max_lat).',
106+
str(cm.exception))
107+
108+
# test first correct bounds configurations
109+
req_files = nightlight.get_required_nl_files(bounds = bounds_c1)
110+
bool = np.array_equal(np.array([1, 1, 1, 1, 1, 1, 0, 0]), req_files)
111+
self.assertTrue(bool)
112+
# second correct configuration
113+
req_files = nightlight.get_required_nl_files(bounds = bounds_c2)
114+
bool = np.array_equal(np.array([0, 0, 1, 1, 1, 1, 0, 0]), req_files)
115+
self.assertTrue(bool)
116+
# third correct configuration
117+
req_files = nightlight.get_required_nl_files(bounds = bounds_c3)
118+
bool = np.array_equal(np.array([0, 0, 0, 0, 0, 0, 1, 0]), req_files)
119+
self.assertTrue(bool)
120+
121+
def test_check_nl_local_file_exists(self):
122+
""" Test that an array with the correct number of already existing files
123+
is produced, the LOGGER messages logged and the ValueError raised. """
124+
125+
# check logger messages by giving a to short req_file
126+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level = 'WARNING') as cm:
127+
nightlight.check_nl_local_file_exists(required_files = np.array([0, 0, 1, 1]))
128+
self.assertIn('The parameter \'required_files\' was too short and '
129+
'is ignored.', cm.output[0])
130+
131+
# check logger message: not all files are available
132+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level = 'DEBUG') as cm:
133+
nightlight.check_nl_local_file_exists()
134+
self.assertIn('Not all satellite files available. '
135+
f'Found 5 out of 8 required files in {Path(SYSTEM_DIR)}', cm.output[0])
136+
137+
# check logger message: no files found in checkpath
138+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level = 'INFO') as cm:
139+
# using a random path where no files are stored
140+
nightlight.check_nl_local_file_exists(check_path = Path('climada/entity/exposures'))
141+
self.assertIn('No satellite files found locally in climada/entity/exposures', cm.output[0])
142+
143+
# test raises with wrong path
144+
with self.assertRaises(ValueError) as cm:
145+
nightlight.check_nl_local_file_exists(check_path = '/random/wrong/path')
146+
self.assertEqual('The given path does not exist: /random/wrong/path', str(cm.exception))
147+
148+
# test that files_exist is correct
149+
files_exist = nightlight.check_nl_local_file_exists()
150+
self.assertEqual(int(sum(files_exist)), 5)
151+
82152
# Execute Tests
83153
if __name__ == "__main__":
84154
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestNightLight)

climada/test/test_litpop_integr.py

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
from climada.util.constants import SYSTEM_DIR
3030
from climada import CONFIG
3131

32+
bounds = (8.41, 47.2, 8.70, 47.45) # (min_lon, max_lon, min_lat, max_lat)
33+
shape = Polygon([
34+
(bounds[0], bounds[3]),
35+
(bounds[2], bounds[3]),
36+
(bounds[2], bounds[1]),
37+
(bounds[0], bounds[1])
38+
])
3239

3340
class TestLitPopExposure(unittest.TestCase):
3441
"""Test LitPop exposure data model:"""
@@ -122,14 +129,7 @@ def test_switzerland300_admin1_pc2016_pass(self):
122129
def test_from_shape_zurich_pass(self):
123130
"""test initiating LitPop for custom shape (square around Zurich City)
124131
Distributing an imaginary total value of 1000 USD"""
125-
bounds = (8.41, 47.2, 8.70, 47.45) # (min_lon, max_lon, min_lat, max_lat)
126132
total_value=1000
127-
shape = Polygon([
128-
(bounds[0], bounds[3]),
129-
(bounds[2], bounds[3]),
130-
(bounds[2], bounds[1]),
131-
(bounds[0], bounds[1])
132-
])
133133
ent = lp.LitPop.from_shape(shape, total_value, res_arcsec=30, reference_year=2016)
134134
self.assertEqual(ent.gdf.value.sum(), 1000.0)
135135
self.assertEqual(ent.gdf.value.min(), 0.0)
@@ -144,13 +144,7 @@ def test_from_shape_zurich_pass(self):
144144
def test_from_shape_and_countries_zurich_pass(self):
145145
"""test initiating LitPop for custom shape (square around Zurich City)
146146
with from_shape_and_countries()"""
147-
bounds = (8.41, 47.2, 8.70, 47.45) # (min_lon, max_lon, min_lat, max_lat)
148-
shape = Polygon([
149-
(bounds[0], bounds[3]),
150-
(bounds[2], bounds[3]),
151-
(bounds[2], bounds[1]),
152-
(bounds[0], bounds[1])
153-
])
147+
154148
ent = lp.LitPop.from_shape_and_countries(
155149
shape, 'Switzerland', res_arcsec=30, reference_year=2016)
156150
self.assertEqual(ent.gdf.value.min(), 0.0)
@@ -186,6 +180,46 @@ def test_Liechtenstein_30_pop_pass(self):
186180
self.assertAlmostEqual(ent.gdf.latitude.max(), 47.2541666666666)
187181
self.assertAlmostEqual(ent.meta['transform'][0], 30/3600)
188182

183+
def test_from_nightlight_intensity(self):
184+
""" Test raises, logger and if methods from_countries and from_shape are
185+
are used."""
186+
187+
with self.assertRaises(ValueError) as cm:
188+
lp.LitPop.from_nightlight_intensity()
189+
self.assertEqual('Either `countries` or `shape` required. Aborting.', str(cm.exception))
190+
191+
with self.assertRaises(ValueError) as cm:
192+
lp.LitPop.from_nightlight_intensity(countries = 'Liechtenstein', shape = shape)
193+
self.assertEqual('Not allowed to set both `countries` and `shape`. Aborting.', str(cm.exception))
194+
195+
exp = lp.LitPop.from_nightlight_intensity(countries = 'Liechtenstein')
196+
self.assertEqual(exp.fin_mode, 'none')
197+
198+
exp = lp.LitPop.from_nightlight_intensity(shape = shape)
199+
self.assertEqual(exp.value_unit, '')
200+
201+
with self.assertLogs('climada.entity.exposures.litpop.litpop', level = 'WARNING') as cm:
202+
lp.LitPop.from_nightlight_intensity(shape = shape)
203+
self.assertIn('Note: set_nightlight_intensity sets values to raw nightlight intensity,', cm.output[0])
204+
205+
def test_from_population(self):
206+
""" Test raises, logger and if methods from_countries and from_shape are
207+
are used."""
208+
209+
with self.assertRaises(ValueError) as cm:
210+
lp.LitPop.from_population()
211+
self.assertEqual('Either `countries` or `shape` required. Aborting.', str(cm.exception))
212+
213+
exp = lp.LitPop.from_population(countries = 'Liechtenstein')
214+
self.assertEqual(exp.fin_mode, 'pop')
215+
216+
exp = lp.LitPop.from_population(shape = shape)
217+
self.assertEqual(exp.value_unit, 'people')
218+
219+
with self.assertRaises(ValueError) as cm:
220+
lp.LitPop.from_population(countries = 'Liechtenstein', shape = shape)
221+
self.assertEqual('Not allowed to set both `countries` and `shape`. Aborting.', str(cm.exception))
222+
189223
class TestAdmin1(unittest.TestCase):
190224
"""Test the admin1 functionalities within the LitPop module"""
191225

@@ -265,20 +299,17 @@ def test_get_gpw_file_path_pass(self):
265299
def test_load_gpw_pop_shape_pass(self):
266300
"""test method gpw_population.load_gpw_pop_shape"""
267301
gpw_version = CONFIG.exposures.litpop.gpw_population.gpw_version.int()
268-
bounds = (8.41, 47.2, 8.70, 47.45) # (min_lon, max_lon, min_lat, max_lat)
269-
shape = Polygon([
270-
(bounds[0], bounds[3]),
271-
(bounds[2], bounds[3]),
272-
(bounds[2], bounds[1]),
273-
(bounds[0], bounds[1])
274-
])
275302
try:
276303
data, meta, glb_transform = \
277304
gpw_population.load_gpw_pop_shape(shape, 2020, gpw_version, verbose=False)
278305
self.assertEqual(data.shape, (31, 36))
279306
self.assertAlmostEqual(meta['transform'][0], 0.00833333333333333)
280307
self.assertAlmostEqual(meta['transform'][0], glb_transform[0])
281-
308+
self.assertEqual(meta['driver'], 'GTiff')
309+
self.assertEqual(meta['height'], data.shape[0])
310+
self.assertEqual(meta['width'], data.shape[1])
311+
self.assertIsInstance(data, np.ndarray)
312+
self.assertEqual(len(data.shape), 2)
282313
except FileExistsError as err:
283314
self.assertIn('lease download', err.args[0])
284315
self.skipTest('GPW input data for GPW v4.%i not found.' %(gpw_version))

0 commit comments

Comments
 (0)