def setUp(self): """Set up cubes.""" temp_vals = [278.0, 280.0, 285.0, 286.0] pressure_vals = [93856.0, 95034.0, 96216.0, 97410.0] data = np.ones((2, 1, 3, 3)) relh_data = np.ones((2, 1, 3, 3)) * 0.65 temperature = set_up_cube(data, 'air_temperature', 'K', realizations=np.array([0, 1])) relative_humidity = set_up_cube(relh_data, 'relative_humidity', '%', realizations=np.array([0, 1])) pressure = set_up_cube(data, 'air_pressure', 'Pa', realizations=np.array([0, 1])) self.height_points = np.array([5., 195., 200.]) self.temperature_cube = set_up_height_cube(self.height_points, cube=temperature) self.relative_humidity_cube = (set_up_height_cube( self.height_points, cube=relative_humidity)) self.pressure_cube = set_up_height_cube(self.height_points, cube=pressure) for i in range(0, 3): self.temperature_cube.data[i, ::] = temp_vals[i + 1] self.pressure_cube.data[i, ::] = pressure_vals[i + 1] # Add hole in middle of data. self.temperature_cube.data[i, :, :, 1, 1] = temp_vals[i] self.pressure_cube.data[i, :, :, 1, 1] = pressure_vals[i] x_coord = iris.coords.DimCoord(np.linspace(-2000, 2000, 3), 'projection_x_coordinate', units='m') y_coord = iris.coords.DimCoord(np.linspace(-2000, 2000, 3), 'projection_y_coordinate', units='m') self.orog = iris.cube.Cube(np.ones((3, 3)), standard_name='surface_altitude', units='m') self.land_sea = iris.cube.Cube(np.ones((3, 3)), standard_name='land_binary_mask', units='m') cubes = [ self.temperature_cube, self.relative_humidity_cube, self.pressure_cube ] for cube in cubes: cube.remove_coord("latitude") cube.remove_coord("longitude") cube.add_dim_coord(x_coord, 3) cube.add_dim_coord(y_coord, 4) cubes = [self.orog, self.land_sea] for cube in cubes: cube.add_dim_coord(x_coord, 0) cube.add_dim_coord(y_coord, 1)
def setUp(self): """Set up cubes for testing.""" data = np.full((3, 1, 3, 3), 275.15, dtype=np.float) cube = add_forecast_reference_time_and_forecast_period( set_up_cube(data, "air_temperature", "Kelvin")) cube = cube[0] cube.remove_coord("realization") self.cube = cube # Cube with multiple times. data = np.full((3, 2, 3, 3), 275.15, dtype=np.float) temp_cube = set_up_cube(data, "air_temperature", "Kelvin", timesteps=2) fp_points = [3.0, 4.0] temp_cubes = iris.cube.CubeList([]) for acube, fp_point in zip(temp_cube.slices_over("time"), fp_points): temp_cubes.append( add_forecast_reference_time_and_forecast_period( acube, time_point=cube.coord("time").points, fp_point=fp_point)) cube_orig = temp_cubes.merge_cube() cube_orig.transpose([1, 0, 2, 3]) cube_orig = cube_orig[0] cube_orig.remove_coord("realization") self.cube_orig = cube_orig # Cube without forecast_period. cube_orig_without_fp = cube_orig.copy() cube_orig_without_fp.remove_coord("forecast_period") self.cube_orig_without_fp = cube_orig_without_fp cube_without_fp = cube.copy() cube_without_fp.remove_coord("forecast_period") self.cube_without_fp = cube_without_fp # Cube with a model, model_id and model realization. cube_orig_model = cube_orig.copy() cube_orig_model.add_aux_coord(AuxCoord([1000, 1000], long_name="model"), data_dims=0) cube_orig_model.add_aux_coord(AuxCoord([ "Operational MOGREPS-UK Model Forecast", "Operational UKV Model Forecast" ], long_name="model_id"), data_dims=0) cube_orig_model.add_aux_coord(AuxCoord([0, 1001], long_name="model_realization"), data_dims=0) cube_orig_model.add_aux_coord(AuxCoord([0, 1], long_name="realization"), data_dims=0) self.cube_orig_model = cube_orig_model self.cube_model = cube_orig_model.collapsed("forecast_reference_time", iris.analysis.MEAN) # Coordinate that is being blended. self.coord = "forecast_reference_time"
def setUp(self): """Set up data for testing.""" self.plugin = GenerateTopographicZoneWeights() orography_data = np.array([[[[10., 25.], [75., 100.]]]]) orography = set_up_cube(orography_data, "altitude", "m", realizations=np.array([0]), y_dimension_length=2, x_dimension_length=2) orography = orography[0, 0, ...] orography.remove_coord("realization") orography.remove_coord("time") self.orography = orography landmask_data = np.array([[0, 1], [1, 1]]) landmask = orography.copy(data=landmask_data) landmask.rename("land_binary_mask") landmask.units = Unit("1") self.landmask = landmask self.thresholds_dict = { 'land': { 'bounds': [[0, 50], [50, 200]], 'units': 'm' } }
def test_simple_check_data(self): """ Test that the plugin returns an Iris.cube.Cube with the expected forecast values at each percentile. """ expected = np.array([8, 10, 12]) expected = expected[:, np.newaxis, np.newaxis, np.newaxis] data = np.array([8, 10, 12]) data = data[:, np.newaxis, np.newaxis, np.newaxis] current_temperature_forecast_cube = ( add_forecast_reference_time_and_forecast_period( set_up_cube(data, "air_temperature", "1", y_dimension_length=1, x_dimension_length=1))) cube = current_temperature_forecast_cube cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = (np.array([10, 50, 90])) percentiles = [10, 50, 90] bounds_pairing = (-40, 50) plugin = Plugin() result = plugin._interpolate_percentiles(cube, percentiles, bounds_pairing, self.perc_coord) self.assertArrayAlmostEqual(result.data, expected)
def set_up_test_cube(): """ Set up a temperature cube with additional global attributes. """ data = (np.linspace(-45.0, 45.0, 9).reshape(1, 1, 3, 3) + 273.15) cube = set_up_cube(data, 'air_temperature', 'K', realizations=([0])) cube.attributes['Conventions'] = 'CF-1.5' cube.attributes['source_realizations'] = np.arange(12) return cube
def test_data_no_mask_three_bands(self): """Test that the result data is as expected, when none of the points are masked and there are three bands defined.""" orography_data = np.array([[[[10., 40., 45.], [70., 80., 95.], [115., 135., 145.]]]]) orography = set_up_cube(orography_data, "altitude", "m", realizations=np.array([0]), y_dimension_length=3, x_dimension_length=3) orography = orography[0, 0, ...] orography.remove_coord("realization") orography.remove_coord("time") landmask_data = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) landmask = orography.copy(data=landmask_data) landmask.rename("land_binary_mask") landmask.units = Unit("1") thresholds_dict = { 'bounds': [[0, 50], [50, 100], [100, 150]], 'units': 'm' } expected_weights_data = np.array([[[1.0, 0.7, 0.6], [0.1, 0.0, 0.0], [0.0, 0.0, 0.0]], [[0.0, 0.3, 0.4], [0.9, 0.9, 0.6], [0.2, 0.0, 0.0]], [[0.0, 0.0, 0.0], [0.0, 0.1, 0.4], [0.8, 1.0, 1.0]]]) result = self.plugin.process(orography, thresholds_dict, landmask) self.assertIsInstance(result, iris.cube.Cube) self.assertArrayAlmostEqual(result.data, expected_weights_data, decimal=2)
def test_percentile_is_dimension_coordinate_multiple_timesteps(self): """ Test that the data has been reshaped correctly when multiple timesteps are in the cube. The array contents is also checked. """ expected = np.array([[[[4., 4.71428571], [5.42857143, 6.14285714]], [[6.85714286, 7.57142857], [8.28571429, 9.]]]]) data = np.tile(np.linspace(5, 10, 8), 3).reshape(3, 2, 2, 2) data[0] -= 1 data[1] += 1 data[2] += 3 cube = set_up_cube(data, "air_temperature", "degreesC", timesteps=2, x_dimension_length=2, y_dimension_length=2) cube.coord("realization").rename("percentile") cube.coord("percentile").points = ( np.array([10, 50, 90])) plen = 1 percentile_cube = ( add_forecast_reference_time_and_forecast_period( cube, time_point=np.array([402295.0, 402296.0]), fp_point=[2.0, 3.0])) reshaped_array = ( restore_non_probabilistic_dimensions( percentile_cube[0].data, percentile_cube, "percentile", plen)) self.assertArrayAlmostEqual(reshaped_array, expected)
def test_lots_of_input_percentiles(self): """ Test that the plugin returns an Iris.cube.Cube with the expected data values for the percentiles, if there are lots of thresholds. """ input_forecast_values_1d = np.linspace(10, 20, 30) input_forecast_values = (np.tile(input_forecast_values_1d, (3, 3, 1, 1)).T) data = np.array([[[[11., 11., 11.], [11., 11., 11.], [11., 11., 11.]]], [[[15., 15., 15.], [15., 15., 15.], [15., 15., 15.]]], [[[19., 19., 19.], [19., 19., 19.], [19., 19., 19.]]]]) percentiles_values = np.linspace(0, 100, 30) cube = (add_forecast_reference_time_and_forecast_period( set_up_cube(input_forecast_values, "air_temperature", "1", realizations=np.arange(30)))) cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = (np.array(percentiles_values)) percentiles = [10, 50, 90] bounds_pairing = (-40, 50) plugin = Plugin() result = plugin._interpolate_percentiles(cube, percentiles, bounds_pairing, self.perc_coord) self.assertArrayAlmostEqual(result.data, data)
def setUp(self): """Set up cubes.""" temp_vals = [278.0, 280.0, 285.0, 286.0] pressure_vals = [93856.0, 95034.0, 96216.0, 97410.0] data = np.ones((2, 1, 3, 3)) relh_data = np.ones((2, 1, 3, 3)) * 0.65 temperature = set_up_cube(data, 'air_temperature', 'K', realizations=np.array([0, 1])) relative_humidity = set_up_cube(relh_data, 'relative_humidity', '%', realizations=np.array([0, 1])) pressure = set_up_cube(data, 'air_pressure', 'Pa', realizations=np.array([0, 1])) self.height_points = np.array([5., 195., 200.]) self.temperature_cube = set_up_height_cube(self.height_points, cube=temperature) self.relative_humidity_cube = (set_up_height_cube( self.height_points, cube=relative_humidity)) self.pressure_cube = set_up_height_cube(self.height_points, cube=pressure) for i in range(0, 3): self.temperature_cube.data[i, ::] = temp_vals[i + 1] self.pressure_cube.data[i, ::] = pressure_vals[i + 1] # Add hole in middle of data. self.temperature_cube.data[i, :, :, 1, 1] = temp_vals[i] self.pressure_cube.data[i, :, :, 1, 1] = pressure_vals[i] self.orog = iris.cube.Cube(np.ones((3, 3)), standard_name='surface_altitude', units='m') self.orog.add_dim_coord( iris.coords.DimCoord(np.linspace(-45.0, 45.0, 3), 'latitude', units='degrees'), 0) self.orog.add_dim_coord( iris.coords.DimCoord(np.linspace(120, 180, 3), 'longitude', units='degrees'), 1)
def test_realization_for_greater_than_check_data_many_realizations(self): """ Test to check the behaviour whether the number of percentiles is greater than the number of realizations. For when the length of the percentiles is greater than the length of the realizations, check that the points of the realization coordinate is as expected. """ data = np.tile(np.linspace(5, 10, 9), 9).reshape(9, 1, 3, 3) data[0] -= 1 data[1] += 1 data[2] += 3 cube = set_up_cube( data, "air_temperature", "degreesC", realizations=np.arange(0, 9)) self.realization_cube = ( add_forecast_reference_time_and_forecast_period(cube.copy())) cube.coord("realization").rename(self.perc_coord) self.percentile_cube = ( add_forecast_reference_time_and_forecast_period(cube)) expected = np.array([[[[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]], [[6., 6.625, 7.25], [7.875, 8.5, 9.125], [9.75, 10.375, 11.]], [[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]], [[6., 6.625, 7.25], [7.875, 8.5, 9.125], [9.75, 10.375, 11.]], [[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]], [[6., 6.625, 7.25], [7.875, 8.5, 9.125], [9.75, 10.375, 11.]], [[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]], [[6., 6.625, 7.25], [7.875, 8.5, 9.125], [9.75, 10.375, 11.]], [[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]]]]) post_processed_forecast_percentiles = self.percentile_cube raw_forecast_realizations = self.realization_cube raw_forecast_realizations = raw_forecast_realizations[:2, :, :, :] plu = Plugin() result = plu._recycle_raw_ensemble_realizations( post_processed_forecast_percentiles, raw_forecast_realizations, self.perc_coord) self.assertArrayAlmostEqual(expected, result.data)
def setUp(self): """Set up percentile cube.""" data = np.tile(np.linspace(5, 10, 9), 3).reshape(3, 1, 3, 3) data[0] -= 1 data[1] += 1 data[2] += 3 self.perc_coord = "percentile_over_realization" cube = set_up_cube(data, "air_temperature", "degreesC") cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = (np.array([10, 50, 90])) self.percentile_cube = ( add_forecast_reference_time_and_forecast_period(cube)) self.perc_coord_mismatch = "percentile_over_nbhood" cube_mismatch = set_up_cube(data, "air_temperature", "degreesC") cube_mismatch.coord("realization").rename(self.perc_coord_mismatch) cube_mismatch.coord(self.perc_coord_mismatch).points = (np.array( [10, 50, 90])) self.percentile_cube_mismatch = ( add_forecast_reference_time_and_forecast_period(cube_mismatch))
def setUp(self): """Set up cube """ data = np.array([0, 1, 5, 11, 20, 5, 9, 10, 4, 2, 0, 1, 29, 30, 1, 5, 6, 6]).reshape(2, 1, 3, 3) self.cube = set_up_cube(data, 'air_temperature', 'K', realizations=np.array([0, 1])) self.wxcode = np.array(WX_DICT.keys()) self.wxmeaning = " ".join(WX_DICT.values()) self.data_directory = mkdtemp() self.nc_file = self.data_directory + '/wxcode.nc' Call(['touch', self.nc_file])
def test_invalid_orography(self): """Test that the appropriate exception is raised if the orography has more than two dimensions.""" orography_data = np.array([[[[0., 25.], [75., 100.]]]]) orography = set_up_cube( orography_data, "altitude", "m", realizations=np.array([0]), y_dimension_length=2, x_dimension_length=2) msg = "The input orography cube should be two-dimensional" with self.assertRaisesRegexp(InvalidCubeError, msg): self.plugin.process(orography, self.thresholds_dict, self.landmask)
def setUp(self): """Create cubes to input.""" self.temperature_cube = set_up_temperature_cube() self.wind_speed_cube = set_up_wind_speed_cube() # create cube with metadata and values suitable for pressure. pressure_data = (np.tile(np.linspace(100000, 110000, 9), 3).reshape(3, 1, 3, 3)) pressure_data[0] -= 2 pressure_data[1] += 2 pressure_data[2] += 4 self.pressure_cube = set_up_cube(pressure_data, "air_pressure", "Pa") # create cube with metadata and values suitable for relative humidity. relative_humidity_data = (np.tile(np.linspace(0, 0.6, 9), 3).reshape(3, 1, 3, 3)) relative_humidity_data[0] += 0 relative_humidity_data[1] += 0.2 relative_humidity_data[2] += 0.4 self.relative_humidity_cube = set_up_cube(relative_humidity_data, "relative_humidity", "1")
def set_up_precipitation_rate_cube(): """Create a cube with metadata and values suitable for precipitation rate.""" data = np.array([[[[0., 1., 2.], [1., 2., 3.], [0., 2., 2.]]]]) cube1 = set_up_cube(data, "lwe_precipitation_rate", "mm/hr", realizations=np.array([0]), timesteps=1) cube1.coord("time").points = [412227.0] cube1.convert_units("m s-1") data = np.array([[[[4., 4., 1.], [4., 4., 1.], [4., 4., 1.]]]]) cube2 = set_up_cube(data, "lwe_precipitation_rate", "mm/hr", realizations=np.array([0]), timesteps=1) cube2.coord("time").points = [412228.0] cube2.convert_units("m s-1") return iris.cube.CubeList([cube1, cube2])
def setUp(self): """Set up realization and percentile cubes for testing.""" data = np.tile(np.linspace(5, 10, 9), 3).reshape(3, 1, 3, 3) data[0] -= 1 data[1] += 1 data[2] += 3 cube = set_up_cube(data, "air_temperature", "degreesC") self.realization_cube = ( add_forecast_reference_time_and_forecast_period(cube.copy())) cube.coord("realization").rename("percentile") cube.coord("percentile").points = (np.array([10, 50, 90])) self.percentile_cube = ( add_forecast_reference_time_and_forecast_period(cube))
def set_up_orographic_enhancement_cube(): """Create a cube with metadata and values suitable for precipitation rate.""" data = np.array([[[[0., 0., 0.], [0., 0., 4.], [1., 1., 2.]]]]) cube1 = set_up_cube(data, "orographic_enhancement", "mm/hr", realizations=np.array([0]), timesteps=1) cube1.coord("time").points = [412227.0] cube1.convert_units("m s-1") data = np.array([[[[5., 5., 5.], [2., 1., 0.], [2., 1., 0.]]]]) cube2 = set_up_cube(data, "orographic_enhancement", "mm/hr", realizations=np.array([0]), timesteps=1) cube2.coord("time").points = [412228.0] cube2.convert_units("m s-1") return iris.cube.CubeList([cube1, cube2]).concatenate_cube()
def setUp(self): """Set up cube """ data = np.array( [0, 1, 5, 11, 20, 5, 9, 10, 4, 2, 0, 1, 29, 30, 1, 5, 6, 6], dtype=np.int32).reshape(2, 1, 3, 3) self.cube = set_up_cube(data, 'air_temperature', 'K', realizations=np.array([0, 1], dtype=np.int32)) self.wxcode = np.array(list(WX_DICT.keys())) self.wxmeaning = " ".join(WX_DICT.values()) self.data_directory = mkdtemp() self.nc_file = self.data_directory + '/wxcode.nc' pathlib.Path(self.nc_file).touch(exist_ok=True)
def set_up_test_cube(): """ Set up a temperature cube with additional global attributes. """ data = (np.linspace(-45.0, 45.0, 9).reshape(1, 1, 3, 3) + 273.15) cube = set_up_cube(data, 'air_temperature', 'K', realizations=([0])) cube.attributes['source_realizations'] = np.arange(12) # Desired attributes that will be global in netCDF file cube.attributes['title'] = 'Operational MOGREPS-UK Forecast Model' cube.attributes['um_version'] = '10.4' cube.attributes['grid_id'] = 'enukx_standard_v1' cube.attributes['source'] = 'Met Office Unified Model' cube.attributes['Conventions'] = 'CF-1.5' cube.attributes['institution'] = 'Met Office' cube.attributes['history'] = '' return cube
def setUp(self): """ Create a cube with a realization coordinate and a cube with a percentile coordinate with forecast_reference_time and forecast_period coordinates. """ data = np.tile(np.linspace(5, 10, 9), 3).reshape(3, 1, 3, 3) data[0] -= 1 data[1] += 1 data[2] += 3 cube = set_up_cube(data, "air_temperature", "degreesC") self.realization_cube = ( add_forecast_reference_time_and_forecast_period(cube.copy())) cube.coord("realization").rename("percentile_over_realization") self.percentile_cube = ( add_forecast_reference_time_and_forecast_period(cube))
def set_up_test_cube(): """ Set up a temperature cube with additional global attributes. """ data = (np.linspace(-45.0, 45.0, 9, dtype=np.float32).reshape(1, 1, 3, 3) + 273.15) cube = set_up_cube(data, 'air_temperature', 'K', realizations=([0])) cube.attributes['source_realizations'] = np.arange(12) # Desired attributes that will be global in netCDF file cube.attributes['um_version'] = '10.4' cube.attributes['mosg__grid_type'] = 'standard' cube.attributes['mosg__model_configuration'] = 'uk_ens' cube.attributes['mosg__grid_domain'] = 'uk_extended' cube.attributes['mosg__grid_version'] = '1.2.0' cube.attributes['source'] = 'Met Office Unified Model' cube.attributes['Conventions'] = 'CF-1.5' cube.attributes['institution'] = 'Met Office' cube.attributes['history'] = '' return cube
def setUp(self): """Set up percentile cube and spot percentile cube.""" data = np.tile(np.linspace(5, 10, 9), 3).reshape(3, 1, 3, 3) data[0] -= 1 data[1] += 1 data[2] += 3 self.perc_coord = "percentile" cube = set_up_cube(data, "air_temperature", "degreesC") cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = (np.array([10, 50, 90])) self.percentile_cube = ( add_forecast_reference_time_and_forecast_period(cube)) spot_cube = (add_forecast_reference_time_and_forecast_period( set_up_spot_temperature_cube())) spot_cube.convert_units("degreesC") spot_cube.coord("realization").rename(self.perc_coord) spot_cube.coord(self.perc_coord).points = (np.array([10, 50, 90])) spot_cube.data = np.tile(np.linspace(5, 10, 3), 9).reshape(3, 1, 9) self.spot_percentile_cube = spot_cube
def test_check_single_threshold(self): """ Test that the plugin returns an Iris.cube.Cube with the expected data values for the percentiles, if a single percentile is used within the input set of percentiles. """ expected = np.array( [[[[4., 4.625, 5.25], [5.875, 6.5, 7.125], [7.75, 8.375, 9.]]], [[[24.44444444, 24.79166667, 25.13888889], [25.48611111, 25.83333333, 26.18055556], [26.52777778, 26.875, 27.22222222]]], [[[44.88888889, 44.95833333, 45.02777778], [45.09722222, 45.16666667, 45.23611111], [45.30555556, 45.375, 45.44444444]]]], dtype=np.float32) data = np.array([8]) data = data[:, np.newaxis, np.newaxis, np.newaxis] current_temperature_forecast_cube = ( add_forecast_reference_time_and_forecast_period( set_up_cube(data, "air_temperature", "1", realizations=[0], y_dimension_length=1, x_dimension_length=1))) cube = current_temperature_forecast_cube cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = np.array([0.2]) for acube in self.percentile_cube.slices_over(self.perc_coord): cube = acube break percentiles = [10, 50, 90] bounds_pairing = (-40, 50) plugin = Plugin() result = plugin._interpolate_percentiles(cube, percentiles, bounds_pairing, self.perc_coord) self.assertArrayAlmostEqual(result.data, expected)
def test_check_data_multiple_timesteps(self): """ Test that the plugin returns an Iris.cube.Cube with the expected data values for the percentiles. """ expected = np.array([[[[4.5, 5.21428571], [5.92857143, 6.64285714]], [[7.35714286, 8.07142857], [8.78571429, 9.5]]], [[[6.5, 7.21428571], [7.92857143, 8.64285714]], [[9.35714286, 10.07142857], [10.78571429, 11.5]]], [[[7.5, 8.21428571], [8.92857143, 9.64285714]], [[10.35714286, 11.07142857], [11.78571429, 12.5]]]]) data = np.tile(np.linspace(5, 10, 8), 3).reshape(3, 2, 2, 2) data[0] -= 1 data[1] += 1 data[2] += 3 cube = set_up_cube(data, "air_temperature", "degreesC", timesteps=2, x_dimension_length=2, y_dimension_length=2) cube.coord("realization").rename(self.perc_coord) cube.coord(self.perc_coord).points = (np.array([10, 50, 90])) self.percentile_cube = ( add_forecast_reference_time_and_forecast_period( cube, time_point=np.array([402295.0, 402296.0]), fp_point=[2.0, 3.0])) cube = self.percentile_cube percentiles = [20, 60, 80] bounds_pairing = (-40, 50) plugin = Plugin() result = plugin._interpolate_percentiles(cube, percentiles, bounds_pairing, self.perc_coord) self.assertArrayAlmostEqual(result.data, expected)