@@ -138,35 +138,42 @@ def iter_chunk_transforms(
138138 yield from _iter_sorted_1d_array_map (sorted_map , storage , dim_grid )
139139 return
140140
141- # Enumerate candidate chunks via the cartesian product of per-dimension
142- # candidate chunk ids, then for each candidate intersect the transform with
143- # the chunk domain (`transform.intersect` handles orthogonal and vectorized
144- # cases alike, filtering out combinations it does not actually touch).
141+ # Enumerate candidate chunks via the cartesian product of per-slot candidate
142+ # chunk ids, then for each candidate intersect the transform with the chunk
143+ # domain (`transform.intersect` handles orthogonal and vectorized cases
144+ # alike, filtering out combinations it does not actually touch).
145145 #
146- # Each dimension contributes exactly the chunk ids it can touch:
146+ # A slot covers one or more output dimensions and contributes exactly the
147+ # chunk-coordinate tuples those dimensions can touch:
147148 #
148- # - `ConstantMap`/`DimensionMap` dims contribute a contiguous `range` — a
149- # single chunk for a constant, and the span between the first and last
150- # chunk for a slice. These are already tight (or nearly so).
151- # - `ArrayMap` (fancy) dims contribute only the *distinct* chunk ids the
152- # index array actually lands in (`np.unique`), never the dense
153- # `range(min_chunk, max_chunk + 1)` between them. A sparse fancy selection
154- # (e.g. two far-apart coordinates) would otherwise enumerate every chunk
155- # in the bounding box, making resolution scale with grid size instead of
156- # with the number of selected coordinates.
157- #
158- # For >= 2 correlated (vindex) ArrayMaps the per-dimension distinct sets
159- # over-approximate the *joint* touched set (their cartesian product includes
160- # combinations no single point lands in), but `intersect` filters those out,
161- # so the yielded chunks are identical either way — and the work stays bounded
162- # by the per-dimension distinct chunk counts, not the grid size.
163- chunk_candidates : list [Sequence [int ]] = []
149+ # - `ConstantMap`/`DimensionMap` dims each form their own slot with a
150+ # contiguous range — a single chunk for a constant, and the span between
151+ # the first and last chunk for a slice. These are already tight (or
152+ # nearly so).
153+ # - Orthogonal `ArrayMap` (fancy) dims each form their own slot with only
154+ # the *distinct* chunk ids the index array actually lands in
155+ # (`np.unique`), never the dense `range(min_chunk, max_chunk + 1)`
156+ # between them. A sparse fancy selection (e.g. two far-apart coordinates)
157+ # would otherwise enumerate every chunk in the bounding box, making
158+ # resolution scale with grid size instead of with the number of selected
159+ # coordinates.
160+ # - Correlated (vindex) `ArrayMap` dims share one *joint* slot holding the
161+ # distinct chunk-coordinate tuples the points actually land in. The
162+ # cartesian product of their per-dimension distinct sets would include
163+ # combinations no point touches — quadratic in the number of selected
164+ # points for a diagonal selection — while the joint distinct set is
165+ # bounded by the point count (see zarr-python gh-4174).
166+ correlated_dims : list [int ] = []
167+ correlated_chunk_ids : list [np .ndarray [Any , np .dtype [np .intp ]]] = []
168+ slot_dims : list [tuple [int , ...]] = []
169+ slot_candidates : list [Sequence [tuple [int , ...]]] = []
164170 for out_dim , m in enumerate (transform .output ):
165171 dg = dim_grids [out_dim ]
166172 if isinstance (m , ConstantMap ):
167173 # Single chunk
168174 c = dg .index_to_chunk (m .offset )
169- chunk_candidates .append ((c ,))
175+ slot_dims .append ((out_dim ,))
176+ slot_candidates .append (((c ,),))
170177 elif isinstance (m , DimensionMap ):
171178 d = m .input_dimension
172179 dim_lo = transform .domain .inclusive_min [d ]
@@ -181,25 +188,48 @@ def iter_chunk_transforms(
181188 s_max = m .offset + m .stride * dim_lo
182189 first = dg .index_to_chunk (s_min )
183190 last = dg .index_to_chunk (s_max )
184- chunk_candidates .append (range (first , last + 1 ))
191+ slot_dims .append ((out_dim ,))
192+ slot_candidates .append ([(c ,) for c in range (first , last + 1 )])
185193 else :
186194 # m: ArrayMap (OutputIndexMap = ConstantMap | DimensionMap | ArrayMap).
187195 # Storage coordinates were already computed for a correlated 1-D map.
188196 storage = (
189197 array_map_1d [1 ] if array_map_1d is not None else m .offset + m .stride * m .index_array
190198 )
191- flat = storage .ravel ().astype (np .intp )
192- if flat .size == 0 :
199+ if storage .size == 0 :
193200 # Empty fancy selection: no coordinates, so no chunks are touched.
194201 return
195- chunk_ids = dg .indices_to_chunks (flat )
196- # Enumerate only the distinct chunks the coordinates land in.
197- chunk_candidates .append ([int (c ) for c in np .unique (chunk_ids )])
202+ # Keep the index-array shape: correlated maps broadcast against each
203+ # other below, and raveling first would lose the singleton axes.
204+ chunk_ids = dg .indices_to_chunks (storage .astype (np .intp ))
205+ if m .input_dimension is None :
206+ correlated_dims .append (out_dim )
207+ correlated_chunk_ids .append (chunk_ids )
208+ else :
209+ slot_dims .append ((out_dim ,))
210+ slot_candidates .append ([(int (c ),) for c in np .unique (chunk_ids )])
211+
212+ if len (correlated_dims ) == 1 :
213+ slot_dims .append ((correlated_dims [0 ],))
214+ slot_candidates .append ([(int (c ),) for c in np .unique (correlated_chunk_ids [0 ])])
215+ elif len (correlated_dims ) >= 2 :
216+ # Group the points jointly: distinct rows of the per-point chunk
217+ # coordinates, O(points log points) regardless of grid size.
218+ broadcast = np .broadcast_arrays (* correlated_chunk_ids )
219+ stacked = np .stack ([b .ravel () for b in broadcast ], axis = 1 )
220+ joint = np .unique (stacked , axis = 0 )
221+ slot_dims .append (tuple (correlated_dims ))
222+ slot_candidates .append ([tuple (int (c ) for c in row ) for row in joint ])
198223
199224 import itertools
200225
201- for chunk_coords_tuple in itertools .product (* chunk_candidates ):
202- chunk_coords = tuple (int (c ) for c in chunk_coords_tuple )
226+ output_rank = len (transform .output )
227+ for combo in itertools .product (* slot_candidates ):
228+ chunk_coords_list = [0 ] * output_rank
229+ for dims , part in zip (slot_dims , combo , strict = True ):
230+ for d , c in zip (dims , part , strict = True ):
231+ chunk_coords_list [d ] = c
232+ chunk_coords = tuple (chunk_coords_list )
203233
204234 # Build the chunk domain in storage space
205235 chunk_min : list [int ] = []
0 commit comments