1212from collections import OrderedDict , namedtuple
1313from copy import deepcopy
1414
15+ import dask .array as da
1516import numpy as np
1617
1718from iris ._lazy_data import (
@@ -430,7 +431,13 @@ def match(self, other, error_on_mismatch):
430431 if self .data_shape != other .data_shape :
431432 msg = "cube.shape differs: {} != {}"
432433 msgs .append (msg .format (self .data_shape , other .data_shape ))
433- if self .data_type != other .data_type :
434+ if (
435+ self .data_type is not None
436+ and other .data_type is not None
437+ and self .data_type != other .data_type
438+ ):
439+ # N.B. allow "None" to match any other dtype: this means that dataless
440+ # cubes can merge with 'dataful' ones.
434441 msg = "cube data dtype differs: {} != {}"
435442 msgs .append (msg .format (self .data_type , other .data_type ))
436443 # Both cell_measures_and_dims and ancillary_variables_and_dims are
@@ -1109,8 +1116,6 @@ def __init__(self, cube):
11091116 source-cube.
11101117
11111118 """
1112- if cube .is_dataless ():
1113- raise iris .exceptions .DatalessError ("merge" )
11141119 # Default hint ordering for candidate dimension coordinates.
11151120 self ._hints = [
11161121 "time" ,
@@ -1240,7 +1245,10 @@ def merge(self, unique=True):
12401245 # their data loaded then at the end we convert the stack back
12411246 # into a plain numpy array.
12421247 stack = np .empty (self ._stack_shape , "object" )
1243- all_have_data = True
1248+ all_have_real_data = True
1249+ some_are_dataless = False
1250+ part_shape : tuple = None
1251+ part_dtype : np .dtype = None
12441252 for nd_index in nd_indexes :
12451253 # Get the data of the current existing or last known
12461254 # good source-cube
@@ -1249,18 +1257,45 @@ def merge(self, unique=True):
12491257 data = self ._skeletons [group [offset ]].data
12501258 # Ensure the data is represented as a dask array and
12511259 # slot that array into the stack.
1252- if is_lazy_data ( data ) :
1253- all_have_data = False
1260+ if data is None :
1261+ some_are_dataless = True
12541262 else :
1255- data = as_lazy_data (data )
1263+ # We have (at least one) array content : Record the shape+dtype
1264+ part_shape = data .shape
1265+ part_dtype = data .dtype
1266+ # ensure lazy (we make the result real, later, if all were real)
1267+ if is_lazy_data (data ):
1268+ all_have_real_data = False
1269+ else :
1270+ data = as_lazy_data (data )
12561271 stack [nd_index ] = data
12571272
1258- merged_data = multidim_lazy_stack (stack )
1259- if all_have_data :
1260- # All inputs were concrete, so turn the result back into a
1261- # normal array.
1262- merged_data = as_concrete_data (merged_data )
1263- merged_cube = self ._get_cube (merged_data )
1273+ if part_shape is None :
1274+ # NO parts had data : the result will also be dataless
1275+ merged_data = None
1276+ merged_shape = self ._shape
1277+ else :
1278+ # At least some inputs had data : the result will have a data array.
1279+ if some_are_dataless :
1280+ # Some parts were dataless: fill these with a lazy all-missing array.
1281+ missing_part = da .ma .masked_array (
1282+ data = da .zeros (part_shape , dtype = part_dtype ),
1283+ mask = da .ones (part_shape , dtype = bool ),
1284+ dtype = part_dtype ,
1285+ )
1286+ for inds in np .ndindex (stack .shape ):
1287+ if stack [inds ] is None :
1288+ stack [inds ] = missing_part
1289+
1290+ # Make a single lazy merged result array
1291+ merged_data = multidim_lazy_stack (stack )
1292+ merged_shape = None
1293+ if all_have_real_data :
1294+ # All inputs were concrete, so turn the result back into a
1295+ # normal array.
1296+ merged_data = as_concrete_data (merged_data )
1297+
1298+ merged_cube = self ._get_cube (merged_data , shape = merged_shape )
12641299 merged_cubes .append (merged_cube )
12651300
12661301 return merged_cubes
@@ -1291,8 +1326,6 @@ def register(self, cube, error_on_mismatch=False):
12911326 this :class:`ProtoCube`.
12921327
12931328 """
1294- if cube .is_dataless ():
1295- raise iris .exceptions .DatalessError ("merge" )
12961329 cube_signature = self ._cube_signature
12971330 other = self ._build_signature (cube )
12981331 match = cube_signature .match (other , error_on_mismatch )
@@ -1545,12 +1578,18 @@ def name_in_independents():
15451578 # deferred loading, this does NOT change the shape.
15461579 self ._shape .extend (signature .data_shape )
15471580
1548- def _get_cube (self , data ):
1581+ def _get_cube (self , data , shape = None ):
15491582 """Generate fully constructed cube.
15501583
15511584 Return a fully constructed cube for the given data, containing
15521585 all its coordinates and metadata.
15531586
1587+ Parameters
1588+ ----------
1589+ data : array_like
1590+ Cube data content. If None, `shape` must be set and the result is dataless.
1591+ shape : tuple, optional
1592+ Cube data shape, only used if data is None.
15541593 """
15551594 signature = self ._cube_signature
15561595 dim_coords_and_dims = [
@@ -1573,6 +1612,7 @@ def _get_cube(self, data):
15731612 aux_coords_and_dims = aux_coords_and_dims ,
15741613 cell_measures_and_dims = cms_and_dims ,
15751614 ancillary_variables_and_dims = avs_and_dims ,
1615+ shape = shape ,
15761616 ** kwargs ,
15771617 )
15781618
0 commit comments