Skip to content

WDF File I/O / WDFファイル入出力

WDF 0.4 is an xarray-backed, HDF5-based artifact for exact typed round-trips of Wandas' seven built-in Frame classes. WDF 0.4 は、Wandas の7種類の built-in Frameを型付きで往復する、xarray backedのHDF5 artifactです。

Contract / 契約

  • BaseFrame.save(path, *, compress="gzip", overwrite=False) saves WDF 0.4.
  • wd.load(path) restores the exact stored built-in Frame type.
  • ChannelFrame.load(path) additionally requires the stored type to be ChannelFrame.
  • Loading accepts local str and Path values only. URL download is not part of the WDF API.
  • WDF 0.1 through 0.3 and future versions are explicitly unsupported. There is no fallback or migration layer.

Root attributes are version, frame_type, sampling_rate, label, constructor_json, metadata_json, and operation_history_json. The xarray Dataset contains these data variables:

data
channel_label
channel_unit
channel_ref
channel_calibration_factor
source_time_offset
channel_extra_json

The stable channel IDs are a dimension coordinate. Other persisted represented axes are ordinary one-dimensional xarray dimension coordinates; the I/O layer does not give one coordinate name a separate storage mechanism. data.dims is the sole source of semantic dimension names. Frequency and local time are derived from sampling_rate, n_fft, and hop_length, so they are not stored.

Raw tensor values and calibration are stored separately. This prevents calibration from being applied twice after load. Runtime lineage, live operation objects, Recipe artifacts, and Dask graphs are outside WDF; operation_history_json is display history only.

保存は同期的に完了しますが、Wandasは内部でchunkをwriterへ渡し、事前にtensor全体を NumPy化しません。WDFから読み込んだFrameと、そのFrameから派生したFrameを利用して いる間は、元のWDFファイルを移動・削除・上書きしないでください。数値は他のFrameと 同じくframe.dataからNumPy配列として取得します。xarrayやDask backendを利用者が 管理する必要はありません。

Saving / 保存

wandas.io.wdf_io.save(frame, path, *, compress='gzip', overwrite=False)

Save an exact built-in Frame as WDF 0.4.

Dask data is handed directly to xarray and is written synchronously; Wandas does not first materialize the complete tensor with frame._data.compute().

Source code in wandas/io/wdf_io.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def save(
    frame: BaseFrame[Any],
    path: str | Path,
    *,
    compress: str | None = "gzip",
    overwrite: bool = False,
) -> None:
    """Save an exact built-in Frame as WDF 0.4.

    Dask data is handed directly to xarray and is written synchronously; Wandas does
    not first materialize the complete tensor with ``frame._data.compute()``.
    """
    target = _normalized_path(path)
    if target.exists() and not overwrite:
        raise FileExistsError(f"File {target} already exists. Set overwrite=True to overwrite.")

    # Validate all Frame and JSON state before either importing the storage backend
    # or opening the destination, so invalid state cannot leave a partial artifact.
    dataset = _build_dataset(frame)
    require_h5netcdf("WDF save")
    encoding = {"data": {"compression": compress}} if compress else None
    dataset.to_netcdf(
        target,
        engine="h5netcdf",
        encoding=encoding,
        invalid_netcdf=True,
    )

Loading / 読み込み

wandas.io.wdf_io.load(path)

Load a local WDF 0.4 artifact as its exact built-in Frame type.

The returned Frame owns access to its source internally. Keep the source path unchanged while that Frame or Frames derived from it are in use. Obtain NumPy values through frame.data without managing the storage backend directly.

Source code in wandas/io/wdf_io.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def load(path: str | Path) -> BaseFrame[Any]:
    """Load a local WDF 0.4 artifact as its exact built-in Frame type.

    The returned Frame owns access to its source internally. Keep the source path
    unchanged while that Frame or Frames derived from it are in use. Obtain NumPy
    values through ``frame.data`` without managing the storage backend directly.
    """
    source = Path(path)
    if not source.exists():
        raise FileNotFoundError(f"File not found: {source}")
    require_h5netcdf("WDF load")

    # CF decoding is disabled because WDF owns dtype/value semantics; foreign CF
    # scale, offset, fill-value, and time attributes must never transform raw data.
    dataset = xr.open_dataset(
        source,
        engine="h5netcdf",
        chunks={},
        decode_cf=False,
        mask_and_scale=False,
        backend_kwargs={"phony_dims": "access"},
    )
    version = dataset.attrs.get("version")
    if version != WDF_FORMAT_VERSION:
        got = "missing" if version is None else repr(version)
        raise ValueError(
            "Unsupported WDF format version\n"
            f"  Got: {got}\n"
            f"  Supported: {WDF_FORMAT_VERSION!r}\n"
            "Use a compatible Wandas version or resave the file."
        )
    _require_exact_schema(dataset)

    frame_type = dataset.attrs["frame_type"]
    if not isinstance(frame_type, str):
        raise ValueError("Invalid WDF frame_type; expected text")
    constructor = _load_json(dataset.attrs["constructor_json"], field="constructor_json")
    metadata = _load_json(dataset.attrs["metadata_json"], field="metadata_json")
    label = _load_json(dataset.attrs["label"], field="label")
    history = _validate_history(_load_json(dataset.attrs["operation_history_json"], field="operation_history_json"))
    if not isinstance(constructor, Mapping):
        raise ValueError("Invalid WDF constructor_json; expected an object")
    if not isinstance(metadata, dict):
        raise ValueError("Invalid WDF metadata_json; expected an object")
    if label is not None and not isinstance(label, str):
        raise ValueError("Invalid WDF label; expected a string or null")
    sampling_rate = _finite_number(dataset.attrs["sampling_rate"], field="sampling_rate")

    data = dataset["data"]
    channel_count = int(dataset.sizes["channel"])
    channel_ids = _text_vector(dataset, "channel", channel_count)
    labels = _text_vector(dataset, "channel_label", channel_count)
    units = _text_vector(dataset, "channel_unit", channel_count)
    refs = _number_vector(dataset, "channel_ref", channel_count)
    factors = _number_vector(dataset, "channel_calibration_factor", channel_count)
    offsets = _number_vector(dataset, "source_time_offset", channel_count)
    extras_json = _text_vector(dataset, "channel_extra_json", channel_count)
    extras = [_load_json(value, field=f"channel_extra_json[{index}]") for index, value in enumerate(extras_json)]
    if not all(isinstance(extra, dict) for extra in extras):
        raise ValueError("Invalid WDF channel_extra_json; expected JSON objects")
    channels = [
        ChannelMetadata(
            label=labels[index],
            calibration=ChannelCalibration(factor=float(factors[index]), unit=units[index], ref=float(refs[index])),
            extra=extras[index],
        )
        for index in range(channel_count)
    ]
    common: dict[str, Any] = {
        "sampling_rate": sampling_rate,
        "label": label,
        "metadata": metadata,
        "channel_metadata": channels,
        "channel_ids": channel_ids,
        "source_time_offset": offsets,
        "operation_history_prefix": history,
    }
    frame = decode_frame(
        frame_type,
        constructor,
        data=data.data,
        common=common,
        stored_dims=cast(tuple[str, ...], tuple(data.dims)),
    )
    coordinates = {
        str(name): np.asarray(coordinate.values) for name, coordinate in dataset.coords.items() if name != "channel"
    }
    restore_frame_coordinates(frame, coordinates)
    return frame
import wandas as wd

frame = wd.read("audio.wav").stft(n_fft=2048)
frame.save("analysis.wdf", compress="gzip", overwrite=True)
restored = wd.load("analysis.wdf")