h5web icon indicating copy to clipboard operation
h5web copied to clipboard

Phase should(?) be unwrapped in complex number visualization

Open NAThompson opened this issue 4 weeks ago • 2 comments

Describe the bug

If a list of complex numbers is provided to H5Web via an HDF5 file, there is a beautiful interface that allows it to be visualized as a magnitude or a phase. However, the phase is always taken in the range [-π, π]:

Image

For any function of roughly linear phase, this behavior is not quite what you want. Instead, you would like to use (say) np.unwrap to make the phase decrease monotonically.

To Reproduce

The following script generates the visualization screenshotted above:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["numpy", "h5py"]
# ///
"""
Generate a small HDF5/NeXus file demonstrating that naive visualization
of complex phase (without np.unwrap) produces discontinuities.

Layout:

/ (root)
  @default = "entry"
  /entry
    @NX_class = "NXentry"
    @default  = "data"
    title
    /data
      @NX_class = "NXdata"
      @signal   = "spectrum"
      @axes     = "frequency"

      frequency       (1D, Hz)
      spectrum        (1D, complex128)
      amplitude       (|spectrum|)
      phase_wrapped   (angle(spectrum) in [-π, π])
      phase_unwrapped (np.unwrap(phase_wrapped))
"""

import numpy as np
import h5py
from pathlib import Path


def write_complex_phase_unwrap_demo(
    filename: str | Path = "phase_unwrap_complex.h5",
    n_points: int = 512,
    f_min: float = -5e3,     # Hz
    f_max: float = 5e3,      # Hz
    delta_t: float = 150e-6, # s, controls phase slope
    f0: float = 2e3,         # Hz, Gaussian width scale
) -> None:
    """
    Create an HDF5 file with a complex NeXus signal for H5Web.

    We define:
        f: frequency axis
        A(f) = exp(-0.5 * (f / f0)^2)
        spectrum(f) = A(f) * exp(-1j * 2π f Δt)

    The true phase is -2π f Δt, i.e. a straight line in f.
    The wrapped phase (np.angle) has 2π jumps, which np.unwrap fixes.
    """
    filename = Path(filename)

    # Frequency axis
    f = np.linspace(f_min, f_max, n_points)  # Hz

    # Amplitude envelope (Gaussian in frequency)
    A = np.exp(-0.5 * (f / f0) ** 2)

    # Linear phase: -2π f Δt (negative slope)
    spectrum = A * np.exp(-1j * 2 * np.pi * f * delta_t)  # complex128

    with h5py.File(filename, "w") as h5:
        # Root: default entry
        h5.attrs["default"] = b"entry"

        # NXentry group
        entry = h5.create_group("entry")
        entry.attrs["NX_class"] = b"NXentry"
        entry.attrs["default"] = b"data"

        entry.create_dataset(
            "title",
            data="Complex spectrum phase unwrap demo",
        )

        # NXdata group
        data = entry.create_group("data")
        data.attrs["NX_class"] = "NXdata"

        # Tell NeXus/H5Web which dataset is the signal and what the axis is
        data.attrs["signal"] = "spectrum"
        data.attrs["axes"] = "frequency"

        # Frequency axis
        d_f = data.create_dataset("frequency", data=f.astype("float64"))
        d_f.attrs["units"] = "Hz"
        d_f.attrs["long_name"] = "Frequency"

        # Complex spectrum (the main point of the demo)
        d_spec = data.create_dataset("spectrum", data=spectrum.astype("complex128"))
        d_spec.attrs["units"] = "a.u."
        d_spec.attrs["long_name"] = "A(f) exp(-i 2π f Δt)"

    print(f"Wrote complex phase unwrap demo to {filename.resolve()}")


if __name__ == "__main__":
    write_complex_phase_unwrap_demo()

Expected behaviour

I believe that almost always an array of complex numbers should have its phase unwrapped before visualization.

Obviously this is a context dependent claim, but personally I can't think of an example where you wouldn't want this behavior, and can think of many examples where you do want it.

Context

  • H5Web in VSCode, version 0.2.1.

Aside

  • This interface is absolutely gorgeous-this is a nitpick.
  • Can the signal "long_name" be placed in | | for amplitude visualization or prepended with arg for phase?

NAThompson avatar Dec 01 '25 22:12 NAThompson