1818from mdio .builder .schemas .v1 .units import AllUnitModel
1919from mdio .builder .schemas .v1 .variable import CoordinateMetadata
2020from mdio .builder .schemas .v1 .variable import VariableMetadata
21+ from mdio .builder .templates .types import CoordinateSpec
2122
2223if TYPE_CHECKING :
2324 from mdio .builder .schemas .v1 .dataset import Dataset
@@ -43,6 +44,13 @@ def __init__(self, data_domain: SeismicDataDomain) -> None:
4344 self ._physical_coord_names : tuple [str , ...] = ()
4445 self ._logical_coord_names : tuple [str , ...] = ()
4546 self ._var_chunk_shape : tuple [int , ...] = ()
47+ self .synthesize_missing_dims : tuple [str , ...] = ()
48+
49+ # TEMPORARY (removed with declare_coordinate_specs): set when grid overrides mutate this
50+ # template in-place (dims collapsed into 'trace', extra coordinates added). Once mutated,
51+ # the runtime layout intentionally diverges from the static declare_coordinate_specs()
52+ # contract, so the drift guard in build_dataset() must not run.
53+ self ._grid_overrides_applied : bool = False
4654
4755 self ._builder : MDIODatasetBuilder | None = None
4856 self ._dim_sizes : tuple [int , ...] = ()
@@ -67,6 +75,45 @@ def _repr_html_(self) -> str:
6775 """Return an HTML representation of the template for Jupyter notebooks."""
6876 return template_repr_html (self )
6977
78+ def declare_coordinate_specs (self ) -> tuple [CoordinateSpec , ...]:
79+ """Declare the non-dimension coordinate specs (name, dims, dtype) for this template.
80+
81+ The ingestion ``SchemaResolver`` uses these specs to determine which trace-header
82+ fields to read and how to rewrite coordinate dimensions under grid overrides.
83+
84+ .. note::
85+ TEMPORARY (to be removed before the next minor release): these specs currently
86+ duplicate the non-dimension coordinates created in :meth:`_add_coordinates`.
87+ :meth:`build_dataset` validates that the two stay in sync (see
88+ :meth:`_validate_declared_coordinate_specs`). Once the ingestion pipeline builds
89+ datasets directly from the resolved schema, ``_add_coordinates`` will be derived
90+ from these specs and the duplication will disappear.
91+
92+ The default implementation assumes every non-dimension coordinate spans **all**
93+ spatial dimensions. Subclasses whose coordinates span only a subset (or use a
94+ non-default dtype) must override this method, otherwise ``build_dataset`` raises.
95+
96+ Returns:
97+ The declared non-dimension coordinate specs.
98+ """
99+ specs = [
100+ CoordinateSpec (
101+ name = coord_name ,
102+ dimensions = self .spatial_dimension_names ,
103+ dtype = ScalarType .FLOAT64 ,
104+ )
105+ for coord_name in self .physical_coordinate_names
106+ ]
107+ specs .extend (
108+ CoordinateSpec (
109+ name = coord_name ,
110+ dimensions = self .spatial_dimension_names ,
111+ dtype = ScalarType .UINT8 if coord_name == "gun" else ScalarType .INT32 ,
112+ )
113+ for coord_name in self .logical_coordinate_names
114+ )
115+ return tuple (specs )
116+
70117 def build_dataset (
71118 self ,
72119 name : str ,
@@ -107,6 +154,10 @@ def build_dataset(
107154 except ValueError as exc : # coordinate may already exist
108155 if "same name twice" not in str (exc ):
109156 raise
157+ # Skip the static drift guard when grid overrides have transformed the template: the
158+ # runtime layout no longer matches the declared (override-free) specs by design.
159+ if not self ._grid_overrides_applied :
160+ self ._validate_declared_coordinate_specs ()
110161 self ._add_variables ()
111162 self ._add_trace_mask ()
112163
@@ -123,6 +174,80 @@ def add_units(self, units: dict[str, AllUnitModel]) -> None:
123174 raise ValueError (msg )
124175 self ._units |= units
125176
177+ def apply_resolved_dimensions (
178+ self ,
179+ dim_names : tuple [str , ...],
180+ chunk_shape : tuple [int , ...],
181+ ) -> None :
182+ """Update the template's dimension layout from a resolved schema.
183+
184+ Supported entry point for the ingestion pipeline to push back dimension names
185+ and chunk shape after the SchemaResolver has applied grid overrides
186+ (e.g. NonBinned, HasDuplicates), instead of mutating private attributes.
187+
188+ Args:
189+ dim_names: Final ordered dimension names.
190+ chunk_shape: Chunk shape matching ``dim_names`` length.
191+
192+ Raises:
193+ ValueError: If ``len(chunk_shape) != len(dim_names)``.
194+ """
195+ if len (chunk_shape ) != len (dim_names ):
196+ msg = f"chunk_shape length { len (chunk_shape )} does not match dim_names length { len (dim_names )} "
197+ raise ValueError (msg )
198+ self ._dim_names = tuple (dim_names )
199+ self ._var_chunk_shape = tuple (chunk_shape )
200+
201+ def _validate_declared_coordinate_specs (self ) -> None :
202+ """Fail the build if :meth:`declare_coordinate_specs` drifted from the built coordinates.
203+
204+ TEMPORARY (to be removed before the next minor release): while
205+ :meth:`declare_coordinate_specs` duplicates the non-dimension coordinates created in
206+ :meth:`_add_coordinates`, this guard ensures the two never diverge in name, dimensions,
207+ or dtype. The ingestion ``SchemaResolver`` trusts the declared specs, so silent drift
208+ would corrupt resolved schemas. The check runs for every template (built-in and
209+ user-defined) on every ``build_dataset`` call that does not apply grid overrides. Grid
210+ overrides mutate the template in-place (collapsing dims into ``trace`` and adding
211+ coordinates), so the runtime layout intentionally diverges from the declared specs and
212+ the guard is skipped for those builds. It is removed once ``_add_coordinates`` is derived
213+ from the resolved schema and the duplication no longer exists.
214+
215+ Raises:
216+ ValueError: If the declared specs do not match the built non-dimension coordinates.
217+ """
218+ dim_names = set (self ._dim_names )
219+ built = {coord .name : coord for coord in self ._builder ._coordinates if coord .name not in dim_names }
220+ declared = {spec .name : spec for spec in self .declare_coordinate_specs ()}
221+
222+ if set (declared ) != set (built ):
223+ built_only = sorted (set (built ) - set (declared ))
224+ declared_only = sorted (set (declared ) - set (built ))
225+ msg = (
226+ f"declare_coordinate_specs() for template { self .name !r} is out of sync with the "
227+ f"coordinates built by _add_coordinates(). Built but not declared: { built_only } . "
228+ f"Declared but not built: { declared_only } . Override declare_coordinate_specs() so "
229+ f"it matches the non-dimension coordinates this template creates."
230+ )
231+ raise ValueError (msg )
232+
233+ for coord_name , spec in declared .items ():
234+ coord = built [coord_name ]
235+ built_dims = tuple (dim .name for dim in coord .dimensions )
236+ if built_dims != spec .dimensions :
237+ msg = (
238+ f"declare_coordinate_specs() for template { self .name !r} declares coordinate "
239+ f"{ coord_name !r} over dimensions { spec .dimensions } , but _add_coordinates() built "
240+ f"it over { built_dims } ."
241+ )
242+ raise ValueError (msg )
243+ if coord .data_type != spec .dtype :
244+ msg = (
245+ f"declare_coordinate_specs() for template { self .name !r} declares coordinate "
246+ f"{ coord_name !r} as { spec .dtype } , but _add_coordinates() built it as "
247+ f"{ coord .data_type } ."
248+ )
249+ raise ValueError (msg )
250+
126251 @property
127252 def name (self ) -> str :
128253 """Returns the name of the template."""
0 commit comments