33import logging
44import warnings
55from collections .abc import Sequence
6- from dataclasses import dataclass
7- from math import floor , prod
6+ from dataclasses import dataclass , field
7+ from math import ceil , floor , prod
8+ from typing import TYPE_CHECKING
89
910import numpy as np
1011
1112from cubed .core .plan import FinalizedPlan
1213from cubed .types import T_RegularChunks , T_Shape
13- from cubed .utils import (
14- normalize_chunks ,
15- )
14+ from cubed .utils import normalize_chunks
1615from cubed .vendor .rechunker .algorithm import (
1716 MAX_STAGES ,
1817 ExcessiveIOWarning ,
2221 consolidate_chunks ,
2322)
2423
24+ if TYPE_CHECKING :
25+ from cubed .array_api .array_object import Array
26+
2527logger = logging .getLogger (__name__ )
2628
2729
@@ -253,19 +255,46 @@ class RechunkCopy:
253255 target_chunks : T_RegularChunks
254256 """The chunks of the target array for this copy operation."""
255257
256- source_aligned : bool | None = None
258+ num_input_blocks : int = field (init = False )
259+ """The number of input blocks read from the source."""
260+
261+ num_output_blocks : int = field (init = False )
262+ """The number of output blocks written to the target."""
263+
264+ source_aligned : bool = field (init = False )
257265 """Are copy chunks aligned with source chunks?"""
258266
259- target_aligned : bool | None = None
267+ target_aligned : bool = field ( init = False )
260268 """
261269 Are copy chunks aligned with target chunks?
262270 If not then irregular chunking must be used.
263271 """
264272
273+ def __post_init__ (self ):
274+ self .num_input_blocks = prod (
275+ ceil (cc / sc ) for cc , sc in zip (self .copy_chunks , self .source_chunks )
276+ )
277+ self .num_output_blocks = prod (
278+ ceil (cc / tc ) for cc , tc in zip (self .copy_chunks , self .target_chunks )
279+ )
280+ self .source_aligned = all (
281+ (cc == n ) or (cc % sc == 0 )
282+ for n , sc , cc in zip (self .shape , self .source_chunks , self .copy_chunks )
283+ )
284+ self .target_aligned = all (
285+ (cc == n ) or (cc % tc == 0 )
286+ for n , tc , cc in zip (self .shape , self .target_chunks , self .copy_chunks )
287+ )
288+
265289
266290@dataclass
267291class RechunkPlan :
292+ arrays : list ["Array" ]
268293 copy_ops : list [RechunkCopy ]
294+ regular : bool = field (init = False )
295+
296+ def __post_init__ (self ):
297+ self .regular = not any (not copy_op .target_aligned for copy_op in self .copy_ops )
269298
270299 def _repr_html_ (self ):
271300 from cubed .diagnostics .widgets import get_template
@@ -275,20 +304,60 @@ def _repr_html_(self):
275304 for copy_op in self .copy_ops :
276305 row = (
277306 copy_op ,
278- svg (normalize_chunks (copy_op .source_chunks , shape = copy_op .copy_chunks )),
279- svg (normalize_chunks (copy_op .target_chunks , shape = copy_op .copy_chunks )),
307+ svg (
308+ normalize_chunks (copy_op .source_chunks , shape = copy_op .copy_chunks ),
309+ size = 80 ,
310+ ),
311+ svg (
312+ normalize_chunks (copy_op .target_chunks , shape = copy_op .copy_chunks ),
313+ size = 80 ,
314+ ),
280315 )
281316 table .append (row )
282317
283318 return get_template ("rechunk_plan.j2" ).render (table = table )
284319
320+ @property
321+ def result (self ):
322+ return self .arrays [- 1 ]
323+
324+ @property
325+ def array_view (self ):
326+ return RechunkPlanArrayView (self .arrays )
327+
328+ @property
329+ def stats (self ):
330+ return RechunkPlanStats .from_plan (self , self .result .plan ())
331+
332+
333+ @dataclass
334+ class RechunkPlanArrayView :
335+ arrays : list ["Array" ]
336+
337+ def _repr_html_ (self ):
338+ from cubed .diagnostics .widgets import get_template
339+
340+ table = []
341+ for array in self .arrays :
342+ row = (
343+ array ,
344+ array .to_svg (size = 120 ),
345+ )
346+ table .append (row )
347+
348+ return get_template ("rechunk_arrays.j2" ).render (table = table )
349+
285350
286351@dataclass
287352class RechunkPlanStats :
288353 num_copy_ops : int
289354 num_tasks : int
290355 total_nbytes_written : int
356+ total_task_iops : int
291357 max_task_iops : int
358+ max_task_input_blocks : int
359+ max_task_output_blocks : int
360+ regular : bool
292361
293362 @classmethod
294363 def from_plan (cls , rechunk_plan : RechunkPlan , plan : FinalizedPlan ):
@@ -302,23 +371,51 @@ def from_plan(cls, rechunk_plan: RechunkPlan, plan: FinalizedPlan):
302371 + d ["pipeline" ].config .num_output_blocks [0 ]
303372 for _ , d in rechunks
304373 ]
374+ total_task_iops = sum (
375+ d ["primitive_op" ].num_tasks
376+ * (
377+ d ["pipeline" ].config .num_input_blocks [0 ]
378+ + d ["pipeline" ].config .num_output_blocks [0 ]
379+ )
380+ for _ , d in rechunks
381+ )
382+ num_task_input_blocks = [
383+ d ["pipeline" ].config .num_input_blocks [0 ] for _ , d in rechunks
384+ ]
385+ num_task_output_blocks = [
386+ d ["pipeline" ].config .num_output_blocks [0 ] for _ , d in rechunks
387+ ]
305388
306389 return cls (
307390 num_copy_ops = len (rechunk_plan .copy_ops ),
308391 num_tasks = plan .num_tasks ,
309392 total_nbytes_written = plan .total_nbytes_written ,
393+ total_task_iops = total_task_iops ,
310394 max_task_iops = max (num_task_iops ),
395+ max_task_input_blocks = max (num_task_input_blocks ),
396+ max_task_output_blocks = max (num_task_output_blocks ),
397+ regular = rechunk_plan .regular ,
311398 )
312399
313400
314401def rechunk_plan (x , chunks , * , min_mem = None , allow_irregular = True ):
315402 from cubed .core .ops import _rechunk_plan
316403
404+ out = x
405+ arrays = [x ]
317406 copy_ops = []
318407 source_chunks = x .chunksize
319408 for copy_chunks , target_chunks in _rechunk_plan (
320409 x , chunks , min_mem = min_mem , allow_irregular = allow_irregular
321410 ):
411+ from .ops import _rechunk
412+
413+ out = _rechunk (out , copy_chunks , target_chunks , allow_irregular = allow_irregular )
414+ arrays .append (out )
322415 copy_ops .append (RechunkCopy (x .shape , source_chunks , copy_chunks , target_chunks ))
323416 source_chunks = target_chunks
324- return RechunkPlan (copy_ops )
417+
418+ # final output must have regular chunking
419+ assert copy_ops [- 1 ].target_aligned
420+
421+ return RechunkPlan (arrays , copy_ops )
0 commit comments