Skip to content

Plotting

Matplotlib bar-plot helpers that consume a Result instance and visualise earth potential rise, branch currents and bus currents either for a specific frequency or as RMS across all frequencies.

Physical / modelling context

A fault calculation produces three families of derived quantities that are routinely inspected:

  • Earth potential rise (EPR) — the magnitude of \(\underline{u}(f)\) per bus, both per frequency and as the RMS \(\sqrt{\sum_f |u(f)|^2}\) across all frequencies. The RMS view is the touch-voltage proxy under EN 50522.
  • Branch currents — the per-frequency phasor of the current flowing through each grounding-conductor branch, signed in the branch traversal direction. This is the shield-current pattern that determines reduction factor and EMC effects.
  • Bus currents — the injected current per bus, i.e. the residual that returns through the local earth path \(1/Z_E\). The sum over all buses equals the fault current scaled by \(r\).

For the time-domain transient solver (groundinsight.simulation.transient) the same plot helpers are mirrored with a _transient suffix and consume a ResultTransient instead.

Example

import groundinsight as gi

# Assume `net` has been solved via gi.run_fault(...).
result = net.results["fault1"]

# EPR bar plot at 50 Hz and as RMS over all frequencies
gi.plot_bus_voltages(result=result, frequencies=[50.0],
                     title="EPR @ 50 Hz")
gi.plot_bus_voltages(result=result,
                     title="EPR (RMS over all frequencies)")

# Branch and bus currents
gi.plot_branch_currents(result=result, frequencies=[50.0],
                        title="Shield I")
gi.plot_bus_currents(result=result, title="Bus injection (RMS)")

# Transient counterpart (after a TransientStudy.solve(...))
gi.plot_epr_transient(result=result_t, title="EPR transient")
gi.plot_branch_current_transient(result=result_t, title="Shield I(t)")

All helpers return the matplotlib.figure.Figure they create, so they integrate cleanly with notebook display and with fig.savefig(...) calls.

Only computed frequencies can be plotted

frequencies= selects from the frequencies the result actually contains; asking for one that was never computed raises a KeyError naming both the requested and the available values:

net = gi.create_network(name="net", frequencies=[50.0])
...
gi.plot_bus_voltages(result=result, frequencies=[250.0])
# KeyError: Frequency [250.0] not present in 'uepr_freq' of any bus;
#           the result was computed for [50.0] Hz. ...

This is not pedantry about arguments. A missing frequency used to be substituted with 0.0, and a bar of height zero on an EPR plot is a statement: "the fifth harmonic causes no earth potential rise at this station". Nothing in the figure distinguished that from "250 Hz was never part of the calculation". A bar of height zero now always means a measured zero.

The same check covers the partial case — a frequency present on some buses and missing on others — because a bar group that mixes measured values with substituted zeros is worse still.

Figure ownership

By default every helper creates its figure through pyplot and leaves it open; the caller owns it. In a notebook that is what you want. In a loop — a soil-resistivity sweep, one plot per scenario — it is not, and matplotlib warns after the twentieth figure. Pass close=True and the helper releases the figure it created before returning:

for rho in (50.0, 100.0, 500.0, 1000.0):
    ...
    fig = gi.plot_bus_voltages(result=result, title=f"rho = {rho}",
                               close=True)
    fig.savefig(f"epr_{rho:.0f}.png")

The returned figure is still complete — every axis, bar and label is on it, and savefig works exactly as before. close=True only unregisters it from pyplot, so it is collected once the last reference goes out of scope. plt.close(fig) after the call remains equivalent.

Drawing into an existing axis

Pass ax= to draw into an axis you created yourself. The helper then leaves the surrounding figure alone: it applies no tight_layout, closes nothing, and returns your figure rather than a new one. This is what makes two scenarios comparable side by side:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(16, 5), sharey=True)
gi.plot_bus_voltages(result=base,   ax=axes[0], title="base case")
gi.plot_bus_voltages(result=outage, ax=axes[1], title="cable out")
fig.tight_layout()

Two combinations are rejected rather than silently ignored:

Combination Why it raises
ax= with figsize= The figure already exists and may hold other panels, so the size cannot be honoured. Size it yourself: plt.subplots(figsize=...), or ax.figure.set_size_inches(...).
ax= with close=True close= releases the figure this call created. With ax= the figure belongs to you, and closing it would take every sibling panel with it.

A figsize that cannot be used raises as well, rather than being replaced by the default: matplotlib accepts figsize=(0, 0) when the figure is created and only fails much later, when it is drawn or saved, so the traceback would point at savefig instead of at the call responsible.

API reference

plotting

Plotting Module.

This module provides functions for visualizing the results of electrical network calculations, including UEPR (Earth Potential Rise) for buses and branch currents. It utilizes Matplotlib to generate plots that can display both frequency-dependent and RMS (Root Mean Square) values for various electrical parameters. These visualizations aid in analyzing the performance and behavior of the electrical network under different fault conditions.

Every helper accepts two optional arguments that control where the plot is drawn and who owns the resulting figure:

ax Draw into an existing :class:matplotlib.axes.Axes instead of creating a figure. This is what makes multi-panel comparisons possible -- one panel per outage scenario, per soil resistivity, per fault location -- and follows the same convention as pandas.DataFrame.plot(ax=...).

close Close the freshly created figure before returning it. The figure object remains fully usable (fig.savefig(...) still works); it is merely no longer held by pyplot's figure manager, which is what a long parameter sweep needs in order not to accumulate figures until matplotlib warns at twenty.

Omitting both reproduces the historical behaviour exactly: a new figure of the helper's default size, registered with pyplot and left open.

plot_branch_current_transient

plot_branch_current_transient(
    result: ResultTransient,
    *,
    branches: Optional[List[str]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Branch current over time",
    show: bool = False,
    ax: Optional[plt.Axes] = None,
    close: bool = False
) -> plt.Figure

Plot the time-domain shield current for one or more observed branches.

Parameters:

Name Type Description Default
result ResultTransient

A transient result.

required
branches list of str

Restrict the plot to a subset of the observed branches. None plots every branch that was set as an observation point.

None
figsize tuple of (float, float)

Figure size in inches. None selects the module default (10, 5). Must not be combined with ax.

None
title str

Plot title.

'Branch current over time'
show bool

Call plt.show() immediately. Defaults to False.

False
ax Axes

Draw into this axis instead of creating a new figure.

None
close bool

Close the created figure before returning it; the figure object stays usable. Cannot be combined with ax. Defaults to False.

False

Returns:

Type Description
Figure

The generated figure, or ax.figure when ax is given.

Raises:

Type Description
ValueError

If branches references a name that was not observed, or if ax is combined with figsize or with close=True.

Notes

Without ax and without close the returned figure is registered with pyplot and stays open until the caller closes it; pass close=True when plotting in a loop.

Source code in src/groundinsight/plotting.py
def plot_branch_current_transient(
    result: "ResultTransient",
    *,
    branches: Optional[List[str]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Branch current over time",
    show: bool = False,
    ax: Optional["plt.Axes"] = None,
    close: bool = False,
) -> "plt.Figure":
    """
    Plot the time-domain shield current for one or more observed branches.

    Parameters
    ----------
    result : ResultTransient
        A transient result.
    branches : list of str, optional
        Restrict the plot to a subset of the observed branches. ``None``
        plots every branch that was set as an observation point.
    figsize : tuple of (float, float), optional
        Figure size in inches. ``None`` selects the module default
        ``(10, 5)``. Must not be combined with ``ax``.
    title : str, optional
        Plot title.
    show : bool, optional
        Call ``plt.show()`` immediately. Defaults to ``False``.
    ax : matplotlib.axes.Axes, optional
        Draw into this axis instead of creating a new figure.
    close : bool, optional
        Close the created figure before returning it; the figure object
        stays usable. Cannot be combined with ``ax``. Defaults to
        ``False``.

    Returns
    -------
    matplotlib.figure.Figure
        The generated figure, or ``ax.figure`` when ``ax`` is given.

    Raises
    ------
    ValueError
        If ``branches`` references a name that was not observed, or if
        ``ax`` is combined with ``figsize`` or with ``close=True``.

    Notes
    -----
    Without ``ax`` and without ``close`` the returned figure is registered
    with ``pyplot`` and stays open until the caller closes it; pass
    ``close=True`` when plotting in a loop.
    """
    available = list(result.i_branch_t.keys())
    selection = branches or available
    missing = [b for b in selection if b not in result.i_branch_t]
    if missing:
        raise ValueError(
            f"Branches not present in result: {missing}. "
            f"Observed branches are: {available}."
        )

    fig, ax, owns_figure = _prepare_axes(
        ax, figsize, close, _DEFAULT_TRANSIENT_FIGSIZE
    )
    for branch_name in selection:
        ax.plot(result.time_s, result.i_branch_t[branch_name], label=branch_name)
    ax.set_xlabel("time / s")
    ax.set_ylabel("current / A")
    ax.set_title(title)
    ax.grid(True, alpha=0.3)
    ax.legend(title="Branch")
    return _finalise(fig, owns_figure, show, close)

plot_branch_currents

plot_branch_currents(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Branch Currents",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional[plt.Axes] = None,
    close: bool = False
) -> plt.Figure

Plot the branch currents for each branch.

Generates a bar plot of branch currents in the network. It can plot either frequency-dependent current magnitudes or RMS current values based on the provided parameters.

Parameters:

Name Type Description Default
result Result

The :class:Result object containing the calculation results.

required
frequencies list of float

Frequencies (in Hz) to plot. If None or empty, RMS current values are plotted. Defaults to None.

None
figsize tuple of (float, float)

Figure size in inches. None selects the module default (12, 6). Must not be combined with ax.

None
title str

Title of the plot. Defaults to "Branch Currents".

'Branch Currents'
yscale (linear, log)

Scale for the y-axis. Defaults to "linear".

'linear'
show bool

Whether to display the plot immediately. Defaults to False.

False
ax Axes

Draw into this axis instead of creating a new figure. Keyword-only.

None
close bool

Close the created figure before returning it; the figure object stays usable. Cannot be combined with ax. Keyword-only. Defaults to False.

False

Returns:

Type Description
Figure

The Matplotlib figure object containing the plot. With ax given this is ax.figure.

Raises:

Type Description
KeyError

If a specified frequency is not present in i_s_freq of any branch, or is missing from some branches while present in others. The message lists the frequencies the result actually contains.

ValueError

If ax is combined with figsize or with close=True.

Notes

Without ax and without close the returned figure is registered with pyplot and stays open until the caller closes it; pass close=True when plotting in a loop.

Examples:

>>> import groundinsight as gi
>>> fig = gi.plot_branch_currents(
...     result=result, frequencies=[50, 60],
... )
Source code in src/groundinsight/plotting.py
def plot_branch_currents(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Branch Currents",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional["plt.Axes"] = None,
    close: bool = False,
) -> "plt.Figure":
    """
    Plot the branch currents for each branch.

    Generates a bar plot of branch currents in the network. It can plot
    either frequency-dependent current magnitudes or RMS current values
    based on the provided parameters.

    Parameters
    ----------
    result : Result
        The :class:`Result` object containing the calculation results.
    frequencies : list of float, optional
        Frequencies (in Hz) to plot. If ``None`` or empty, RMS current
        values are plotted. Defaults to ``None``.
    figsize : tuple of (float, float), optional
        Figure size in inches. ``None`` selects the module default
        ``(12, 6)``. Must not be combined with ``ax``.
    title : str, optional
        Title of the plot. Defaults to ``"Branch Currents"``.
    yscale : {'linear', 'log'}, optional
        Scale for the y-axis. Defaults to ``"linear"``.
    show : bool, optional
        Whether to display the plot immediately. Defaults to ``False``.
    ax : matplotlib.axes.Axes, optional
        Draw into this axis instead of creating a new figure. Keyword-only.
    close : bool, optional
        Close the created figure before returning it; the figure object
        stays usable. Cannot be combined with ``ax``. Keyword-only.
        Defaults to ``False``.

    Returns
    -------
    matplotlib.figure.Figure
        The Matplotlib figure object containing the plot. With ``ax``
        given this is ``ax.figure``.

    Raises
    ------
    KeyError
        If a specified frequency is not present in ``i_s_freq`` of any
        branch, or is missing from some branches while present in others.
        The message lists the frequencies the result actually contains.
    ValueError
        If ``ax`` is combined with ``figsize`` or with ``close=True``.

    Notes
    -----
    Without ``ax`` and without ``close`` the returned figure is registered
    with ``pyplot`` and stays open until the caller closes it; pass
    ``close=True`` when plotting in a loop.

    Examples
    --------
    >>> import groundinsight as gi  # doctest: +SKIP
    >>> fig = gi.plot_branch_currents(  # doctest: +SKIP
    ...     result=result, frequencies=[50, 60],
    ... )
    """
    # Extract branch names
    branch_names = [branch.name for branch in result.branches]

    # Initialize data structure for plotting
    current_data = {}

    if frequencies:
        _check_frequencies(frequencies, result.branches, "i_s_freq", "branch")
        # Plot frequency-dependent branch currents
        for freq in frequencies:
            current_values = []
            for branch in result.branches:
                current_complex = branch.i_s_freq.get(freq)
                # ``is not None``: see the note in plot_bus_voltages.
                if current_complex is not None:
                    # Calculate magnitude of the complex current
                    current_magnitude = abs(
                        complex(current_complex.real, current_complex.imag)
                    )
                else:
                    current_magnitude = 0.0  # Handle missing data
                current_values.append(current_magnitude)
            current_data[freq] = current_values

        # Plotting
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        bar_width = 0.8 / len(
            frequencies
        )  # Adjust bar width based on the number of frequencies
        indices = range(len(branch_names))
        for i, (freq, current_values) in enumerate(current_data.items()):
            positions = [x + i * bar_width for x in indices]
            ax.bar(positions, current_values, width=bar_width, label=f"{freq} Hz")
        ax.set_yscale(yscale)
        ax.set_xlabel("Branch Name")
        ax.set_ylabel("Current (A)")
        ax.set_title(title)
        ax.set_xticks(
            [x + bar_width * (len(frequencies) - 1) / 2 for x in indices]
        )
        ax.set_xticklabels(branch_names, rotation=45, ha="right")
        ax.legend(title="Frequency")
        ax.grid(True, axis="y")

    else:
        # Plot RMS values of branch currents
        current_rms_values = []
        for branch in result.branches:
            current_rms = branch.i_s  # RMS value of branch current
            if current_rms is not None:
                current_rms_values.append(current_rms)
            else:
                current_rms_values.append(0.0)  # Handle missing data

        # Plotting
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        ax.bar(branch_names, current_rms_values, label="RMS")
        ax.set_yscale(yscale)
        ax.set_xlabel("Branch Name")
        ax.set_ylabel("Current RMS (A)")
        ax.set_title(title)
        _rotate_xticklabels(ax)
        ax.legend()
        ax.grid(True, axis="y")

    return _finalise(fig, owns_figure, show, close)

plot_bus_currents

plot_bus_currents(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Bus Currents",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional[plt.Axes] = None,
    close: bool = False
) -> plt.Figure

Plot the bus currents for each bus.

Generates a bar plot of bus currents in the network. It can plot either frequency-dependent current magnitudes or RMS current values based on the provided parameters.

Parameters:

Name Type Description Default
result Result

The :class:Result object containing the calculation results.

required
frequencies list of float

Frequencies (in Hz) to plot. If None or empty, RMS current values are plotted. Defaults to None.

None
figsize tuple of (float, float)

Figure size in inches. None selects the module default (12, 6). Must not be combined with ax.

None
title str

Title of the plot. Defaults to "Bus Currents".

'Bus Currents'
yscale (linear, log)

Scale for the y-axis. Defaults to "linear".

'linear'
show bool

Whether to display the plot immediately. Defaults to False.

False
ax Axes

Draw into this axis instead of creating a new figure. Keyword-only.

None
close bool

Close the created figure before returning it; the figure object stays usable. Cannot be combined with ax. Keyword-only. Defaults to False.

False

Returns:

Type Description
Figure

The Matplotlib figure object containing the plot. With ax given this is ax.figure.

Raises:

Type Description
KeyError

If a specified frequency is not present in ia_freq of any bus, or is missing from some buses while present in others. The message lists the frequencies the result actually contains.

ValueError

If ax is combined with figsize or with close=True.

Notes

Without ax and without close the returned figure is registered with pyplot and stays open until the caller closes it; pass close=True when plotting in a loop.

Examples:

>>> import groundinsight as gi
>>> fig = gi.plot_bus_currents(
...     result=result, frequencies=[50, 60],
... )
Source code in src/groundinsight/plotting.py
def plot_bus_currents(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "Bus Currents",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional["plt.Axes"] = None,
    close: bool = False,
) -> "plt.Figure":
    """
    Plot the bus currents for each bus.

    Generates a bar plot of bus currents in the network. It can plot
    either frequency-dependent current magnitudes or RMS current values
    based on the provided parameters.

    Parameters
    ----------
    result : Result
        The :class:`Result` object containing the calculation results.
    frequencies : list of float, optional
        Frequencies (in Hz) to plot. If ``None`` or empty, RMS current
        values are plotted. Defaults to ``None``.
    figsize : tuple of (float, float), optional
        Figure size in inches. ``None`` selects the module default
        ``(12, 6)``. Must not be combined with ``ax``.
    title : str, optional
        Title of the plot. Defaults to ``"Bus Currents"``.
    yscale : {'linear', 'log'}, optional
        Scale for the y-axis. Defaults to ``"linear"``.
    show : bool, optional
        Whether to display the plot immediately. Defaults to ``False``.
    ax : matplotlib.axes.Axes, optional
        Draw into this axis instead of creating a new figure. Keyword-only.
    close : bool, optional
        Close the created figure before returning it; the figure object
        stays usable. Cannot be combined with ``ax``. Keyword-only.
        Defaults to ``False``.

    Returns
    -------
    matplotlib.figure.Figure
        The Matplotlib figure object containing the plot. With ``ax``
        given this is ``ax.figure``.

    Raises
    ------
    KeyError
        If a specified frequency is not present in ``ia_freq`` of any bus,
        or is missing from some buses while present in others. The message
        lists the frequencies the result actually contains.
    ValueError
        If ``ax`` is combined with ``figsize`` or with ``close=True``.

    Notes
    -----
    Without ``ax`` and without ``close`` the returned figure is registered
    with ``pyplot`` and stays open until the caller closes it; pass
    ``close=True`` when plotting in a loop.

    Examples
    --------
    >>> import groundinsight as gi  # doctest: +SKIP
    >>> fig = gi.plot_bus_currents(  # doctest: +SKIP
    ...     result=result, frequencies=[50, 60],
    ... )
    """
    # Extract bus names
    bus_names = [bus.name for bus in result.buses]

    # Initialize data structure for plotting
    current_data = {}

    if frequencies:
        _check_frequencies(frequencies, result.buses, "ia_freq", "bus")
        # Plot frequency-dependent bus currents
        for freq in frequencies:
            current_values = []
            for bus in result.buses:
                current_complex = bus.ia_freq.get(freq)
                # ``is not None``: see the note in plot_bus_voltages.
                if current_complex is not None:
                    # Calculate magnitude of the complex current
                    current_magnitude = abs(
                        complex(current_complex.real, current_complex.imag)
                    )
                else:
                    current_magnitude = 0.0  # Handle missing data
                current_values.append(current_magnitude)
            current_data[freq] = current_values

        # Plotting
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        bar_width = 0.8 / len(
            frequencies
        )  # Adjust bar width based on the number of frequencies
        indices = range(len(bus_names))
        for i, (freq, current_values) in enumerate(current_data.items()):
            positions = [x + i * bar_width for x in indices]
            ax.bar(positions, current_values, width=bar_width, label=f"{freq} Hz")
        ax.set_yscale(yscale)
        ax.set_xlabel("Bus Name")
        ax.set_ylabel("Current (A)")
        ax.set_title(title)
        ax.set_xticks(
            [x + bar_width * (len(frequencies) - 1) / 2 for x in indices]
        )
        ax.set_xticklabels(bus_names, rotation=45, ha="right")
        ax.legend(title="Frequency")
        ax.grid(True, axis="y")

    else:
        # Plot RMS values of bus currents
        current_rms_values = []
        for bus in result.buses:
            current_rms = bus.ia  # RMS value of bus current
            if current_rms is not None:
                current_rms_values.append(current_rms)
            else:
                current_rms_values.append(0.0)  # Handle missing data

        # Plotting
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        ax.bar(bus_names, current_rms_values, label="RMS")
        ax.set_yscale(yscale)
        ax.set_xlabel("Bus Name")
        ax.set_ylabel("Current RMS (A)")
        ax.set_title(title)
        _rotate_xticklabels(ax)
        ax.legend()
        ax.grid(True, axis="y")

    return _finalise(fig, owns_figure, show, close)

plot_bus_voltages

plot_bus_voltages(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "UEPR vs Bus Name",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional[plt.Axes] = None,
    close: bool = False
) -> plt.Figure

Plot the UEPR (Earth Potential Rise) for each bus.

Generates a bar plot of UEPR values for each bus in the network. It can plot either frequency-dependent UEPR magnitudes or RMS UEPR values based on the provided parameters. Additionally, the y-axis scale can be linear or logarithmic.

Parameters:

Name Type Description Default
result Result

The :class:Result object containing the calculation results.

required
frequencies list of float

Frequencies (in Hz) to plot. If None or empty, RMS UEPR values are plotted. Defaults to None.

None
figsize tuple of (float, float)

Figure size in inches as a (width, height) tuple. None selects the module default (12, 6). Must not be combined with ax.

None
title str

Title of the plot. Defaults to "UEPR vs Bus Name".

'UEPR vs Bus Name'
yscale (linear, log)

Scale for the y-axis. Defaults to "linear".

'linear'
show bool

Whether to display the plot immediately. If False, the figure is returned for further manipulation. Defaults to False.

False
ax Axes

Draw into this axis instead of creating a new figure. The figure that owns ax is returned, so a multi-panel figure comes back unchanged from every call. Keyword-only.

None
close bool

Close the created figure with plt.close before returning it. The returned figure stays usable -- fig.savefig(...) works -- it is simply no longer registered with pyplot. Use this in parameter sweeps. Cannot be combined with ax. Keyword-only. Defaults to False.

False

Returns:

Type Description
Figure

The Matplotlib figure object containing the plot. With ax given this is ax.figure, i.e. the caller's own figure.

Raises:

Type Description
KeyError

If a specified frequency is not present in uepr_freq of any bus, or is missing from some buses while present in others. The message lists the frequencies the result actually contains.

ValueError

If ax is combined with figsize or with close=True.

Notes

Without ax and without close the returned figure is registered with pyplot and stays open until the caller closes it. In a loop -- a parameter sweep over specific_earth_resistance, for instance -- pass close=True (or call plt.close(fig) yourself), otherwise matplotlib accumulates the figures and warns after 20.

tight_layout is only applied to figures this call created; a caller-supplied ax leaves the surrounding layout alone.

Examples:

>>> import groundinsight as gi
>>> fig = gi.plot_bus_voltages(
...     result=result, frequencies=[50, 60], yscale="log",
... )

Two scenarios side by side in one figure:

>>> import matplotlib.pyplot as plt
>>> fig, axes = plt.subplots(1, 2, figsize=(16, 5), sharey=True)
>>> gi.plot_bus_voltages(result=base, ax=axes[0], title="base")
>>> gi.plot_bus_voltages(result=outage, ax=axes[1], title="cable out")
Source code in src/groundinsight/plotting.py
def plot_bus_voltages(
    result: Result,
    frequencies: Optional[List[float]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "UEPR vs Bus Name",
    yscale: str = "linear",
    show: bool = False,
    *,
    ax: Optional["plt.Axes"] = None,
    close: bool = False,
) -> "plt.Figure":
    """
    Plot the UEPR (Earth Potential Rise) for each bus.

    Generates a bar plot of UEPR values for each bus in the network. It
    can plot either frequency-dependent UEPR magnitudes or RMS UEPR
    values based on the provided parameters. Additionally, the y-axis
    scale can be linear or logarithmic.

    Parameters
    ----------
    result : Result
        The :class:`Result` object containing the calculation results.
    frequencies : list of float, optional
        Frequencies (in Hz) to plot. If ``None`` or empty, RMS UEPR
        values are plotted. Defaults to ``None``.
    figsize : tuple of (float, float), optional
        Figure size in inches as a ``(width, height)`` tuple. ``None``
        selects the module default ``(12, 6)``. Must not be combined with
        ``ax``.
    title : str, optional
        Title of the plot. Defaults to ``"UEPR vs Bus Name"``.
    yscale : {'linear', 'log'}, optional
        Scale for the y-axis. Defaults to ``"linear"``.
    show : bool, optional
        Whether to display the plot immediately. If ``False``, the figure
        is returned for further manipulation. Defaults to ``False``.
    ax : matplotlib.axes.Axes, optional
        Draw into this axis instead of creating a new figure. The figure
        that owns ``ax`` is returned, so a multi-panel figure comes back
        unchanged from every call. Keyword-only.
    close : bool, optional
        Close the created figure with ``plt.close`` before returning it.
        The returned figure stays usable -- ``fig.savefig(...)`` works --
        it is simply no longer registered with ``pyplot``. Use this in
        parameter sweeps. Cannot be combined with ``ax``. Keyword-only.
        Defaults to ``False``.

    Returns
    -------
    matplotlib.figure.Figure
        The Matplotlib figure object containing the plot. With ``ax``
        given this is ``ax.figure``, i.e. the caller's own figure.

    Raises
    ------
    KeyError
        If a specified frequency is not present in ``uepr_freq`` of any
        bus, or is missing from some buses while present in others. The
        message lists the frequencies the result actually contains.
    ValueError
        If ``ax`` is combined with ``figsize`` or with ``close=True``.

    Notes
    -----
    Without ``ax`` and without ``close`` the returned figure is registered
    with ``pyplot`` and stays open until the caller closes it. In a loop --
    a parameter sweep over ``specific_earth_resistance``, for instance --
    pass ``close=True`` (or call ``plt.close(fig)`` yourself), otherwise
    matplotlib accumulates the figures and warns after 20.

    ``tight_layout`` is only applied to figures this call created; a
    caller-supplied ``ax`` leaves the surrounding layout alone.

    Examples
    --------
    >>> import groundinsight as gi  # doctest: +SKIP
    >>> fig = gi.plot_bus_voltages(  # doctest: +SKIP
    ...     result=result, frequencies=[50, 60], yscale="log",
    ... )

    Two scenarios side by side in one figure:

    >>> import matplotlib.pyplot as plt  # doctest: +SKIP
    >>> fig, axes = plt.subplots(1, 2, figsize=(16, 5), sharey=True)  # doctest: +SKIP
    >>> gi.plot_bus_voltages(result=base, ax=axes[0], title="base")  # doctest: +SKIP
    >>> gi.plot_bus_voltages(result=outage, ax=axes[1], title="cable out")  # doctest: +SKIP
    """
    # Extract bus names
    bus_names = [bus.name for bus in result.buses]

    # Initialize data structure for plotting
    uepr_data = {}

    if frequencies:
        _check_frequencies(frequencies, result.buses, "uepr_freq", "bus")
        # Plot frequency-dependent UEPR values
        for freq in frequencies:
            uepr_values = []
            for bus in result.buses:
                uepr_complex = bus.uepr_freq.get(freq)
                # ``is not None`` rather than a truth test: ComplexNumber is a
                # Pydantic model without ``__bool__``, so 0+0j is truthy today
                # -- but relying on that to mean "present" is an accident, and
                # a genuine 0 V at one frequency must not read as missing.
                if uepr_complex is not None:
                    # Calculate magnitude of the complex UEPR value
                    uepr_magnitude = abs(complex(uepr_complex.real, uepr_complex.imag))
                else:
                    uepr_magnitude = 0.0  # Handle missing data
                uepr_values.append(uepr_magnitude)
            uepr_data[freq] = uepr_values

        # Plotting
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        bar_width = 0.8 / len(frequencies)
        indices = range(len(bus_names))
        for i, (freq, uepr_values) in enumerate(uepr_data.items()):
            positions = [x + i * bar_width for x in indices]
            ax.bar(positions, uepr_values, width=bar_width, label=f"{freq} Hz")

        ax.set_xticks([x + bar_width * (len(frequencies) - 1) / 2 for x in indices])
        ax.set_xticklabels(bus_names, rotation=45, ha="right")
    else:
        # Plot RMS values of UEPR
        uepr_rms_values = [
            bus.uepr if bus.uepr is not None else 0.0 for bus in result.buses
        ]
        fig, ax, owns_figure = _prepare_axes(
            ax, figsize, close, _DEFAULT_BAR_FIGSIZE
        )
        ax.bar(bus_names, uepr_rms_values, label="RMS")

    # Configure plot
    ax.set_yscale(yscale)
    ax.set_xlabel("Bus Name")
    ax.set_ylabel("UEPR (V)")
    ax.set_title(title)
    _rotate_xticklabels(ax)
    ax.legend(title="Frequency")
    ax.grid(True, axis="y")

    return _finalise(fig, owns_figure, show, close)

plot_epr_transient

plot_epr_transient(
    result: ResultTransient,
    *,
    buses: Optional[List[str]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "EPR over time",
    show: bool = False,
    ax: Optional[plt.Axes] = None,
    close: bool = False
) -> plt.Figure

Plot the time-domain EPR for one or more observed buses.

Parameters:

Name Type Description Default
result ResultTransient

A result returned by :meth:groundinsight.simulation.TransientStudy.solve.

required
buses list of str

Restrict the plot to a subset of the observed buses. None plots every bus that was set as an observation point.

None
figsize tuple of (float, float)

Figure size in inches. None selects the module default (10, 5). Must not be combined with ax.

None
title str

Plot title. Defaults to "EPR over time".

'EPR over time'
show bool

Call plt.show() immediately. Defaults to False; the figure is always returned for further customisation.

False
ax Axes

Draw into this axis instead of creating a new figure -- for example to stack EPR and shield current in one two-row figure.

None
close bool

Close the created figure before returning it; the figure object stays usable. Cannot be combined with ax. Defaults to False.

False

Returns:

Type Description
Figure

The generated figure, or ax.figure when ax is given.

Raises:

Type Description
ValueError

If buses references a name that was not observed, or if ax is combined with figsize or with close=True.

Notes

Without ax and without close the returned figure is registered with pyplot and stays open until the caller closes it; pass close=True when plotting in a loop.

Examples:

>>> import matplotlib.pyplot as plt
>>> fig, (top, bottom) = plt.subplots(2, 1, sharex=True)
>>> gi.plot_epr_transient(result=res, ax=top)
>>> gi.plot_branch_current_transient(result=res, ax=bottom)
Source code in src/groundinsight/plotting.py
def plot_epr_transient(
    result: "ResultTransient",
    *,
    buses: Optional[List[str]] = None,
    figsize: Optional[Tuple[float, float]] = None,
    title: str = "EPR over time",
    show: bool = False,
    ax: Optional["plt.Axes"] = None,
    close: bool = False,
) -> "plt.Figure":
    """
    Plot the time-domain EPR for one or more observed buses.

    Parameters
    ----------
    result : ResultTransient
        A result returned by
        :meth:`groundinsight.simulation.TransientStudy.solve`.
    buses : list of str, optional
        Restrict the plot to a subset of the observed buses. ``None``
        plots every bus that was set as an observation point.
    figsize : tuple of (float, float), optional
        Figure size in inches. ``None`` selects the module default
        ``(10, 5)``. Must not be combined with ``ax``.
    title : str, optional
        Plot title. Defaults to ``"EPR over time"``.
    show : bool, optional
        Call ``plt.show()`` immediately. Defaults to ``False``; the figure
        is always returned for further customisation.
    ax : matplotlib.axes.Axes, optional
        Draw into this axis instead of creating a new figure -- for
        example to stack EPR and shield current in one two-row figure.
    close : bool, optional
        Close the created figure before returning it; the figure object
        stays usable. Cannot be combined with ``ax``. Defaults to
        ``False``.

    Returns
    -------
    matplotlib.figure.Figure
        The generated figure, or ``ax.figure`` when ``ax`` is given.

    Raises
    ------
    ValueError
        If ``buses`` references a name that was not observed, or if ``ax``
        is combined with ``figsize`` or with ``close=True``.

    Notes
    -----
    Without ``ax`` and without ``close`` the returned figure is registered
    with ``pyplot`` and stays open until the caller closes it; pass
    ``close=True`` when plotting in a loop.

    Examples
    --------
    >>> import matplotlib.pyplot as plt  # doctest: +SKIP
    >>> fig, (top, bottom) = plt.subplots(2, 1, sharex=True)  # doctest: +SKIP
    >>> gi.plot_epr_transient(result=res, ax=top)  # doctest: +SKIP
    >>> gi.plot_branch_current_transient(result=res, ax=bottom)  # doctest: +SKIP
    """
    available = list(result.epr_t.keys())
    selection = buses or available
    missing = [b for b in selection if b not in result.epr_t]
    if missing:
        raise ValueError(
            f"Buses not present in result: {missing}. "
            f"Observed buses are: {available}."
        )

    fig, ax, owns_figure = _prepare_axes(
        ax, figsize, close, _DEFAULT_TRANSIENT_FIGSIZE
    )
    for bus_name in selection:
        ax.plot(result.time_s, result.epr_t[bus_name], label=bus_name)
    ax.set_xlabel("time / s")
    ax.set_ylabel("EPR / V")
    ax.set_title(title)
    ax.grid(True, alpha=0.3)
    ax.legend(title="Bus")
    return _finalise(fig, owns_figure, show, close)