1313from xarray_einstats .einops import rearrange as xr_rearrange # noqa: F401
1414
1515from ocean_emulators .constants import (
16+ Boundary ,
1617 BoundaryVarNames ,
1718 Example ,
1819 GridMask ,
@@ -138,9 +139,9 @@ def inference_target(self, step: int | slice):
138139 label = self ._get_label (x_index )
139140 return label
140141
141- def get_initial_input (self ):
142- data = self .__getitem__ (0 )[ 0 ]
143- return data
142+ def get_initial_input (self ) -> tuple [ Prognostic , Boundary ] :
143+ prog , boundary , _ = self .__getitem__ (0 )
144+ return prog , boundary
144145
145146 def get_target_time (self , start_step : int , num_steps : int ):
146147 x_index = self ._get_x_index (start_step )
@@ -154,20 +155,28 @@ def get_target_time(self, start_step: int, num_steps: int):
154155 )
155156 )
156157
157- def merge_prognostic_and_boundary (self , prognostic : torch .Tensor , step : int ):
158+ def get_boundary_for_prognostic (
159+ self , prognostic : Prognostic , step : int
160+ ) -> tuple [Prognostic , Boundary ]:
161+ """Return ``(prognostic, boundary)`` at the requested step.
162+
163+ Prognostic is passed through verbatim; boundary is fetched from the
164+ boundary source and returned on the same device as ``prognostic``. The
165+ two tensors may live on different spatial grids once the cross-
166+ resolution boundary source lands — the encoder is responsible for
167+ fusing them.
168+ """
158169 x_index = self ._get_x_index (step )
159170 boundary = self ._get_boundary (x_index ).to (prognostic .device )
160- data = torch .cat ((prognostic , boundary ), dim = 1 )
161- return data
171+ return prognostic , boundary
162172
163173 @elapsed (level = logging .DEBUG )
164174 def __getitem__ (self , idx ):
165175 x_index = self ._get_x_index (idx )
166- data_in = self ._get_prognostic (x_index )
176+ data_in_prog = self ._get_prognostic (x_index )
167177 data_in_boundary = self ._get_boundary (x_index )
168- data_in = torch .cat ((data_in , data_in_boundary ), dim = 1 )
169178 label = self ._get_label (x_index )
170- return (data_in , label )
179+ return (data_in_prog , data_in_boundary , label )
171180
172181 def _get_x_index (self , idx ):
173182 if isinstance (idx , slice ):
@@ -357,44 +366,40 @@ def pin_memory(self):
357366class TrainData :
358367 """A single batch of training data.
359368
360- A single batch contains multiple steps worth of `Example`s (i.e., input/output pairs). These steps are used during
361- autoregressive rollout in the training and inference process.
362-
363- Constraint: The `Input` tensor is a combination of (flattened) prognostic variables (at all depth levels) and
364- boundary forcings. The top `num_prognostic_channels` number of channels must be prognostic variables whereas the
365- remaining bottom channels are boundary forcings.
369+ A single batch contains multiple steps worth of ``Example`` entries, each
370+ of which is a ``(prognostic_input, boundary_input, label)`` triple. The
371+ prognostic and boundary tensors are carried *separately* (they are no
372+ longer concatenated along the channel dim) so that the encoder can fuse
373+ them at the token level, potentially at different spatial resolutions.
366374 """
367375
368- def __init__ (self , num_prognostic_channels : int , ctx : GridContext ):
376+ def __init__ (
377+ self , num_prognostic_channels : int , num_boundary_channels : int , ctx : GridContext
378+ ):
369379 self .num_prognostic_channels = num_prognostic_channels
380+ self .num_boundary_channels = num_boundary_channels
370381 self .ctx = ctx
371382 self .example_by_step : list [Example ] = []
372383 self .load_stats : LoadStats | None = None
373384
374- def append (self , input_ : Input , label : Prognostic ):
385+ def append (
386+ self , prognostic_input : Prognostic , boundary_input : Boundary , label : Prognostic
387+ ) -> None :
375388 """Add another Example as a new step."""
376- self .example_by_step .append ((input_ , label ))
389+ self .example_by_step .append ((prognostic_input , boundary_input , label ))
377390
378- def get_initial_input (self ) -> Input :
391+ def get_initial_input (self ) -> tuple [ Prognostic , Boundary ] :
379392 return self .get_input (0 )
380393
381- def get_input (self , step : int ) -> Input :
382- return self [step ][0 ]
394+ def get_input (self , step : int ) -> tuple [Prognostic , Boundary ]:
395+ prog , boundary , _ = self .example_by_step [step ]
396+ return prog , boundary
383397
384398 def get_label (self , step : int ) -> Prognostic :
385- return self [step ][1 ]
386-
387- def merge_prognostic_and_boundary (self , prognostic : torch .Tensor , step : int ):
388- input_ = self .get_input (step )
389- merged = input_ .clone ()
390- merged [:, : self .num_prognostic_channels ] = prognostic
391- return merged
392-
393- def values (self ):
394- return self .example_by_step
399+ return self .example_by_step [step ][2 ]
395400
396401 def __getitem__ (self , step : int ) -> Example :
397- """Converts index (step) into (data, label) tuple ."""
402+ """Converts index (step) into (prognostic, boundary, label) triple ."""
398403 return self .example_by_step [step ]
399404
400405 def __len__ (self ) -> int :
@@ -405,16 +410,20 @@ def __iter__(self):
405410
406411 def to (self , device : torch .device ) -> None :
407412 for step in self :
413+ prog , boundary , label = self .example_by_step [step ]
408414 self .example_by_step [step ] = (
409- self [step ][0 ].to (device , non_blocking = True ),
410- self [step ][1 ].to (device , non_blocking = True ),
415+ prog .to (device , non_blocking = True ),
416+ boundary .to (device , non_blocking = True ),
417+ label .to (device , non_blocking = True ),
411418 )
412419
413420 def pin_memory (self ):
414421 for step in self :
422+ prog , boundary , label = self .example_by_step [step ]
415423 self .example_by_step [step ] = (
416- self [step ][0 ].pin_memory (),
417- self [step ][1 ].pin_memory (),
424+ prog .pin_memory (),
425+ boundary .pin_memory (),
426+ label .pin_memory (),
418427 )
419428 return self
420429
@@ -484,6 +493,7 @@ def __init__(
484493 self ._concurrent_compute = concurrent_compute_
485494
486495 self .num_prognostic_channels : int = (hist + 1 ) * len (prognostic_var_names )
496+ self .num_boundary_channels : int = (hist + 1 ) * len (boundary_var_names )
487497 assert np .array_equal (srcs [0 ].data .time , srcs [- 1 ].data .time ), (
488498 "src and dst DataSource have different time slices!"
489499 )
@@ -605,9 +615,13 @@ def to_train_data(
605615 Returns:
606616 TrainData with tensors on the target device
607617 """
608- train_data = TrainData (self .num_prognostic_channels , self .ctx .to (device ))
618+ train_data = TrainData (
619+ self .num_prognostic_channels ,
620+ self .num_boundary_channels ,
621+ self .ctx .to (device ),
622+ )
609623 for input_ , boundary , label in raw_train_data .raw_data :
610- input_ , label = self ._to_example (
624+ prog_input , boundary_input , label_tensor = self ._to_example (
611625 OceanData .from_data_source (
612626 input_ ,
613627 self .wet_prognostic [0 ],
@@ -622,43 +636,27 @@ def to_train_data(
622636 label , self .wet_prognostic [- 1 ], self .prognostic_srcs [- 1 ]
623637 ).to (device = device , non_blocking = True ),
624638 )
625- train_data .append (input_ , label )
639+ train_data .append (prog_input , boundary_input , label_tensor )
626640 train_data .load_stats = raw_train_data .load_stats
627641 return train_data
628642
629643 def _to_example (
630644 self , input_ : OceanData , boundary : OceanData , label : OceanData
631- ) -> tuple [ Input , Prognostic ] :
645+ ) -> Example :
632646 # Input/boundary only include current steps; label only includes forecasted steps.
633- total_input = self ._prep_tensor_steps (input_ , boundary )
647+ prog_input = self ._prep_tensor_steps (input_ )
648+ boundary_input = self ._prep_tensor_steps (boundary )
634649 label_tensor = self ._prep_tensor_steps (label )
635- return total_input , label_tensor
650+ return prog_input , boundary_input , label_tensor
636651
637- def _prep_tensor_steps (
638- self ,
639- prognostic : OceanData ,
640- boundary : OceanData | None = None ,
641- ) -> Input :
642- """Prepare tensor steps by normalizing, masking and flattening dimensions."""
643- prognostic_steps = prognostic .normalize_and_mask (
652+ def _prep_tensor_steps (self , ocean_data : OceanData ) -> Input :
653+ """Normalize, mask, and flatten (time, variable) dims into a channel dim."""
654+ steps = ocean_data .normalize_and_mask (
644655 self .normalize_before_mask , self .masked_fill_value
645656 )
646-
647- # Flatten time and variable dimensions
648- def flatten_dims (tensor : torch .Tensor ) -> torch .Tensor :
649- return rearrange (
650- tensor , "batch time variable lat lon -> batch (time variable) lat lon"
651- )
652-
653- prognostic_steps = flatten_dims (prognostic_steps )
654- if boundary is not None :
655- boundary_steps = boundary .normalize_and_mask (
656- self .normalize_before_mask , self .masked_fill_value
657- )
658- boundary_steps = flatten_dims (boundary_steps )
659- return torch .cat ((prognostic_steps , boundary_steps ), dim = 1 )
660-
661- return prognostic_steps
657+ return rearrange (
658+ steps , "batch time variable lat lon -> batch (time variable) lat lon"
659+ )
662660
663661 def _get_x_index (self , idx : int , step : int ) -> xr .DataArray :
664662 assert isinstance (idx , int )
0 commit comments