TPFA_ResSim.plotting

Convenient plot functions for reservoir model.

  1"""Convenient plot functions for reservoir model."""
  2
  3from typing import TYPE_CHECKING, Any, Optional, cast
  4
  5import matplotlib as mpl
  6import matplotlib.pyplot as plt
  7import numpy as np
  8from matplotlib.ticker import MultipleLocator
  9from mpl_tools import is_inline, place, place_ax
 10from mpl_tools.misc import axprops
 11
 12if TYPE_CHECKING:
 13    from TPFA_ResSim import ResSim
 14
 15coord_type = "absolute"
 16"""Define scaling of `Plot2D.plt_field` axes.
 17- "relative": `(0, 1)  x (0, 1)`
 18- "absolute": `(0, Lx) x (0, Ly)`
 19- "index"   : `(0, Ny) x (0, Ny)`
 20"""
 21
 22# Colormap for saturation
 23lin_cm = mpl.colors.LinearSegmentedColormap.from_list
 24cm_ow = lin_cm("", [(0, "#1d9e97"), (0.3, "#b2e0dc"), (1, "#f48974")])
 25# cOil, cWater = "red", "blue"        # Plain
 26# cOil, cWater = "#d8345f", "#01a9b4" # Pastel/neon
 27# cOil, cWater = "#e58a8a", "#086972" # Pastel
 28# ccnvrt = lambda c: np.array(mpl.colors.colorConverter.to_rgb(c))
 29# cMiddle = .3*ccnvrt(cWater) + .7*ccnvrt(cOil)
 30# cm_ow = lin_cm("", [cWater, cMiddle, cOil])
 31
 32
 33styles: dict = dict(
 34    default=dict(
 35        title="",
 36        transf=lambda x: x,
 37        cmap="viridis",
 38        levels=10,
 39        cticks=None,
 40        # Note that providing vmin/vmax (and not a levels list) to mpl
 41        # yields prettier colobar ticks, but destorys the consistency
 42        # of the colorbars from one figure to another.
 43        locator=None,
 44    ),
 45    oil=dict(
 46        title="Oil saturation",
 47        transf=lambda x: 1 - x,
 48        cmap=cm_ow,
 49        levels=np.linspace(0 - 1e-7, 1 + 1e-7, 20),
 50        cticks=np.linspace(0, 1, 6),
 51    ),
 52)
 53"""Default `Plot2D.plt_field` plot styling values."""
 54
 55
 56class Plot2D:
 57    """Plots specialized for 2D fields.
 58
 59    This mixin is not standalone but reads grid and well attributes of the
 60    `ResSim` it gets composed into, rather than re-declaring (avoids stale).
 61    """
 62
 63    def plt_field(
 64        self: "ResSim",
 65        ax: Any,
 66        Z: np.ndarray,
 67        style: str = "default",
 68        wells: Any = True,
 69        argmax: bool = False,
 70        colorbar: Any = True,
 71        labels: bool = True,
 72        grid: bool = False,
 73        finalize: bool = True,
 74        **kwargs,
 75    ) -> Any:
 76        """Contour-plot of the (flat) unravelled field `Z`.
 77
 78        `kwargs` falls back to `styles[style]`, which falls back to `styles['defaults']`.
 79        """
 80        # Populate kwargs with fallback style
 81        kwargs = {**styles["default"], **styles[style], **kwargs}
 82        # Pop from kwargs. Remainder goes to countourf
 83        ax.set(**axprops(kwargs))
 84        cticks = kwargs.pop("cticks")
 85
 86        # Why extent=(0, Lx, 0, Ly), rather than merely changing ticks?
 87        # set_aspect("equal") and mouse hovering (reporting x,y).
 88        if "rel" in coord_type:
 89            Lx, Ly = 1, 1
 90        elif "abs" in coord_type:
 91            Lx, Ly = self.Lx, self.Ly
 92        elif "ind" in coord_type:
 93            Lx, Ly = self.Nx, self.Ny
 94        else:
 95            raise ValueError(f"Unsupported coord_type: {coord_type}")
 96
 97        # Apply transform
 98        Z = np.asarray(Z)
 99        Z = kwargs.pop("transf")(Z)
100
101        # Need to transpose coz orientation is model.shape==(Nx, Ny),
102        # while contour() displays the same orientation as array printing.
103        Z = Z.reshape(self.shape).T
104
105        # Did we bother to specify set_over/set_under/set_bad ?
106        has_out_of_range = getattr(kwargs["cmap"], "_rgba_over", None) is not None
107
108        # Unlike `ax.imshow(Z[::-1])`, `contourf` does not simply fill pixels/cells (but
109        # it does provide nice interpolation!) so there will be whitespace on the margins.
110        # No fix is needed, and anyway it would not be trivial/fast,
111        # ref https://github.com/matplotlib/basemap/issues/406 .
112        collections = ax.contourf(
113            Z,
114            **kwargs,
115            # origin=None,  # ⇒ NB: falsely stretches the field!!!
116            origin="lower",
117            extent=(0, Lx, 0, Ly),
118            extend="both" if has_out_of_range else "neither",
119        )
120
121        # Contourf does not plot (at all) the bad regions. "Fake it" by facecolor
122        if has_out_of_range:
123            ax.set_facecolor(getattr(kwargs["cmap"], "_rgba_bad", "w"))
124
125        # Grid (reflecting the model grid)
126        # NB: If not showing grid, then don't locate ticks on grid, because they're
127        #     generally uglier that mpl's default/automatic tick location. But, it
128        #     should be safe to go with 'g' format instead of 'f'.
129        ax.xaxis.set_major_formatter("{x:g}")
130        ax.yaxis.set_major_formatter("{x:g}")
131        ax.tick_params(which="minor", length=0, color="r")
132        ax.tick_params(which="major", width=1.5, direction="in")
133        if grid:
134            n1 = 10
135            xStep = 1 + self.Nx // n1
136            yStep = 1 + self.Ny // n1
137            ax.xaxis.set_major_locator(MultipleLocator(self.hx * xStep))
138            ax.yaxis.set_major_locator(MultipleLocator(self.hy * yStep))
139            ax.xaxis.set_minor_locator(MultipleLocator(self.hx))
140            ax.yaxis.set_minor_locator(MultipleLocator(self.hy))
141            ax.grid(True, which="both")
142
143        # Axis lims
144        ax.set_xlim((0, Lx))
145        ax.set_ylim((0, Ly))
146        ax.set_aspect("equal")
147        # Axis labels
148        if labels:
149            if "abs" in coord_type:
150                ax.set_xlabel("x")
151                ax.set_ylabel("y")
152            else:
153                ax.set_xlabel(f"x ({coord_type})")
154                ax.set_ylabel(f"y ({coord_type})")
155
156        # Add well markers, grouped (and numbered) by the sign of their rates,
157        # ref `TPFA_ResSim.wells.Wells.signs`. The producers come first, so
158        # that their numbers and colors match those of `plt_production`.
159        if wells and self.wells.nComp:
160            sgn = self.wells.signs
161            # Label the completions by their well's name, if there are any
162            names = None
163            if self.wells.names is not None and self.wells.group is not None:
164                names = np.asarray(self.wells.names)[self.wells.group]
165            if wells == "color":
166                # Colors matching `plt_production` of the producers
167                wells = cast(
168                    dict, {"color": [f"C{i}" for i in range(int(np.sum(sgn < 0)))]}
169                )
170            elif wells in [True, 1]:
171                wells = {}
172            for s in [-1, +1, 0]:
173                if np.any(sgn == s):  # NB: skip, lest empty artists upset the layout
174                    kws = dict(wells)  # NB: copy -- the labels are per sign
175                    if names is not None:
176                        kws.setdefault("text", names[sgn == s])
177                    self.well_scatter(ax, self.wells.xy[sgn == s], s, **kws)
178                wells.pop("color", None)  # producers only
179
180        # Add argmax marker
181        if argmax:
182            idx = Z.T.argmax()  # reverse above transpose
183            xy = self.ind2xy(idx)
184            for c, ms in zip(["b", "r", "y"], [10, 6, 3]):
185                ax.plot(*xy, "o", c=c, ms=ms, label="max", zorder=98)
186
187        # Add colorbar
188        if colorbar:
189            if isinstance(colorbar, type(ax)):
190                cax = dict(cax=colorbar)
191            else:
192                cax = dict(ax=ax, shrink=0.8)
193            ax.figure.colorbar(collections, **cax, ticks=cticks)
194
195        tight_show(ax.figure, finalize)
196        return collections
197
198    def well_scatter(
199        self: "ResSim",
200        ax: Any,
201        ww: np.ndarray,
202        sgn: int = 1,
203        text: Any = None,
204        color: Any = None,  # e.g. "k", or a list of colors (one per well)
205        size: float = 1,
206    ) -> Any:
207        """Scatter-plot the wells of `ww` onto a `Plot2D.plt_field`.
208
209        The marker reflects `sgn`: injector (`+1`), producer (`-1`),
210        or neutral (`0`, i.e. of undecided sign, ref `TPFA_ResSim.wells.Wells.signs`).
211
212        The label, `text`, is either one string for all of them, one *per* well
213        of `ww` (a list), or `False` for none.
214
215        .. note:: The default labels are indices *within* `ww`, not global ones.
216
217            I.e. with `text=None`, and since `plt_field` calls this once per
218            sign, the producers are numbered as `plt_production` numbers them --
219            separately from the injectors, and not as in the unified
220            `TPFA_ResSim.wells.Wells.xy`. But `plt_field` supplies the names of
221            `TPFA_ResSim.wells.Wells.names`, if the wells have been given any.
222        """
223        # Well coordinates
224        ww = self.sub2xy(*self.xy2sub(*ww.T)).T
225        # NB: make sure ww array data is not overwritten (avoid in-place)
226        # fmt: off
227        if   "rel" in coord_type: s = 1/self.Lx, 1/self.Ly                     # noqa
228        elif "abs" in coord_type: s = 1, 1                                     # noqa
229        elif "ind" in coord_type: s = self.Nx/self.Lx, self.Ny/self.Ly         # noqa
230        else: raise ValueError("Unsupported coordinate type: %s" % coord_type) # noqa
231        # fmt: on
232        ww = ww * s
233
234        # Style
235        if sgn > 0:
236            c = "w"
237            ec = "gray"
238            d = "k"
239            m = "v"
240        elif sgn < 0:
241            c = "k"
242            ec = "gray"
243            d = "w"
244            m = "^"
245        else:
246            c = "lightgray"
247            ec = "gray"
248            d = "k"
249            m = "o"
250
251        if color:
252            c = color
253
254        # Markers
255        sh = ax.plot(*ww.T, "r.", ms=3, clip_on=False)
256        sh = ax.scatter(
257            *ww.T,
258            s=(size * 26) ** 2,
259            c=c,
260            marker=m,
261            ec=ec,
262            clip_on=False,
263            zorder=1.5,  # required on Jupypter
264        )
265
266        # Text labels
267        if text is not False:
268            if text is None:
269                labels: Any = range(len(ww))
270            elif isinstance(text, str):
271                labels = len(ww) * [text]
272            else:
273                labels = text
274            for lbl, w in zip(labels, ww):
275                if sgn < 0:
276                    w[1] -= 0.01
277                ax.text(
278                    *w[:2],
279                    lbl,
280                    color=d,
281                    fontsize=size * 12,
282                    ha="center",
283                    va="center",
284                )
285
286        return sh
287
288    def plt_production(
289        self: "ResSim",
290        ax: Any,
291        production: np.ndarray,
292        obs: np.ndarray | None = None,
293        legend_outside: bool = True,
294        finalize: bool = True,
295        labels: Any = None,
296    ) -> list:
297        """Production time series. Multiple wells in 1 axes => not ensemble compat.
298
299        The curves are labelled by their index in `production`, unless `labels`
300        (e.g. a selection of `TPFA_ResSim.wells.Wells.names`) says otherwise.
301        """
302        hh = []
303        tt = 1 + np.arange(len(production))
304        for i, p in enumerate(1 - production.T):
305            hh += ax.plot(tt, p, "-", label=i if labels is None else labels[i])
306
307        if obs is not None:
308            for i, y in enumerate(1 - obs.T):
309                ax.plot(tt, y, "*", c=hh[i].get_color())
310
311        # Add legend
312        if legend_outside:
313            kws = dict(
314                bbox_to_anchor=(1, 1),
315                loc="upper left",
316                ncol=1 + len(production.T) // 10,
317            )
318        else:
319            kws = dict(loc="lower left")
320        ax.legend(title="Well" if labels is not None else "Well #.", **kws)
321
322        ax.set_title("Oil saturation in producers")
323        ax.set_xlabel("Time index")
324        # ax.set_ylim(-0.01, 1.01)
325        ax.axhline(0, c="xkcd:light grey", ls="--", zorder=1.8)
326        ax.axhline(1, c="xkcd:light grey", ls="--", zorder=1.8)
327
328        tight_show(ax.figure, finalize)
329        return hh
330
331    # Note: See note in mpl_setup.py about properly displaying the animation.
332    def anim(
333        self: "ResSim",
334        wsats: np.ndarray,
335        prod: np.ndarray,
336        title: str = "",
337        figsize: tuple = (10, 3.5),
338        pause: int = 200,
339        animate: bool = True,
340        **kwargs,
341    ) -> Any:
342        """Animate the saturation and production time series."""
343
344        # Create figure and axes
345        title = "Animation" + ("-- " + title if title else "")
346        fig, (ax1, ax2) = place.freshfig(
347            title, ncols=2, figsize=figsize, gridspec_kw=dict(width_ratios=(2, 3))
348        )
349        fig.suptitle(title)  # coz animation never (any backend) displays title
350        # Saturations
351        kwargs.update(wells="color", colorbar=True, finalize=False)
352        ax2.cc = self.plt_field(ax2, wsats[-1], "oil", **kwargs)
353        # Production
354        hh = self.plt_production(ax1, prod, legend_outside=False, finalize=False)
355        fig.tight_layout()
356
357        if animate:
358            from matplotlib import animation
359
360            tt = np.arange(len(wsats))
361
362            def update_fig(iT):
363                # Update field.
364                # NB: since mpl 3.8 the `ContourSet` is itself an artist
365                # (it no longer has a `.collections` attribute).
366                try:
367                    ax2.cc.remove()
368                except ValueError:
369                    pass  # occurs when re-running script
370                kwargs.update(wells=False, colorbar=False)
371                ax2.cc = self.plt_field(ax2, wsats[iT], "oil", **kwargs)
372
373                # Update production lines
374                if iT >= 1:
375                    for h, p in zip(hh, prod.T):
376                        h.set_data(tt[1 : 1 + iT], 1 - p[:iT])
377
378            ani = animation.FuncAnimation(
379                fig,
380                update_fig,
381                len(tt),
382                blit=False,
383                interval=pause,
384                # Prevent busy/idle indicator constantly flashing, despite %%capture
385                # and even manually clearing the output of the calling cell.
386                repeat=False,  # flashing stops once the (unshown) animation finishes.
387                # An alternative solution is to do this in the next cell:
388                # animation.event_source.stop()
389                # but it does not work if using "run all", even with time.sleep(1).
390            )
391
392            return ani
393
394
395def tight_show(figure: Any, enabled: bool) -> None:
396    if enabled:
397        figure.tight_layout()
398        plt.show()
399
400
401def _get_ipython() -> Any:
402    """Return the active IPython shell, or `None` (also if IPython isn't installed)."""
403    try:
404        from IPython import get_ipython
405    except ImportError:
406        return None
407    return get_ipython()
408
409
410def _ipython_will_prompt(ip: Any) -> bool:
411    """Whether IPython returns to its (event-loop running) prompt after this script.
412
413    False for `ipython script.py` (no `-i`), which exits immediately.
414    """
415    if ip is None:
416        return False
417    return bool(getattr(ip.parent, "interact", True))
418
419
420def show(block: Optional[bool] = None) -> None:
421    """Display the figures, whether run as script, in IPython, or in a notebook.
422
423    Reasons why a plain `plt.show()` does not suffice:
424
425    - In a script (`python script.py`) it does the right thing (blocks, thereby
426      running the GUI event loop) *only* if interactive mode (`plt.ion()`) is off.
427    - In IPython (e.g. `%run script.py`) the figures stay unpainted until the GUI
428      event loop gets to run, which requires the "input hook" installed by the
429      `%matplotlib` magic (which is why they only appear upon `ctrl-d`/exit).
430      Blocking here would be wrong: it would freeze the prompt.
431    - In Jupyter/Colab with the `inline` backend there is no event loop at all:
432      the (static) figures are rendered by `plt.show()` itself.
433    """
434    ip = _get_ipython()
435
436    if ip is not None and not is_inline():
437        # I.e. the `%matplotlib` magic: activate the event loop ("input hook")
438        # of the current backend. No-op if already active.
439        ip.run_line_magic("matplotlib", "")
440
441    if block is None:
442        # Run the event loop ourselves only if nobody else will.
443        block = not is_inline() and not _ipython_will_prompt(ip)
444
445    plt.show(block=block)
coord_type = 'absolute'

Define scaling of Plot2D.plt_field axes.

  • "relative": (0, 1) x (0, 1)
  • "absolute": (0, Lx) x (0, Ly)
  • "index" : (0, Ny) x (0, Ny)
@staticmethod
def lin_cm(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None):
1194    @staticmethod
1195    def from_list(name, colors, N=256, gamma=1.0, *, bad=None, under=None, over=None):
1196        """
1197        Create a `LinearSegmentedColormap` from a list of colors.
1198
1199        Parameters
1200        ----------
1201        name : str
1202            The name of the colormap.
1203        colors : list of :mpltype:`color` or list of (value, color)
1204            If only colors are given, they are equidistantly mapped from the
1205            range :math:`[0, 1]`; i.e. 0 maps to ``colors[0]`` and 1 maps to
1206            ``colors[-1]``.
1207            If (value, color) pairs are given, the mapping is from *value*
1208            to *color*. This can be used to divide the range unevenly. The
1209            values must increase monotonically from 0 to 1.
1210        N : int
1211            The number of RGB quantization levels.
1212        gamma : float
1213
1214        bad : :mpltype:`color`, default: transparent
1215            The color for invalid values (NaN or masked).
1216        under : :mpltype:`color`, default: color of the lowest value
1217            The color for low out-of-range values.
1218        over : :mpltype:`color`, default: color of the highest value
1219            The color for high out-of-range values.
1220        """
1221        if not np.iterable(colors):
1222            raise ValueError('colors must be iterable')
1223
1224        try:
1225            # Assume the passed colors are a list of colors
1226            # and not a (value, color) tuple.
1227            r, g, b, a = to_rgba_array(colors).T
1228            vals = np.linspace(0, 1, len(colors))
1229        except Exception as e:
1230            # Assume the passed values are a list of
1231            # (value, color) tuples.
1232            try:
1233                _vals, _colors = itertools.zip_longest(*colors)
1234            except Exception as e2:
1235                raise e2 from e
1236            vals = np.asarray(_vals)
1237            if np.min(vals) < 0 or np.max(vals) > 1 or np.any(np.diff(vals) < 0):
1238                raise ValueError(
1239                    "the values passed in the (value, color) pairs "
1240                    "must increase monotonically from 0 to 1."
1241                )
1242            r, g, b, a = to_rgba_array(_colors).T
1243
1244        cdict = {
1245            "red": np.column_stack([vals, r, r]),
1246            "green": np.column_stack([vals, g, g]),
1247            "blue": np.column_stack([vals, b, b]),
1248            "alpha": np.column_stack([vals, a, a]),
1249        }
1250
1251        return LinearSegmentedColormap(name, cdict, N, gamma,
1252                                       bad=bad, under=under, over=over)

Create a LinearSegmentedColormap from a list of colors.

Parameters

name : str The name of the colormap. colors : list of :mpltype:color or list of (value, color) If only colors are given, they are equidistantly mapped from the range \( [0, 1] \); i.e. 0 maps to colors[0] and 1 maps to colors[-1]. If (value, color) pairs are given, the mapping is from value to color. This can be used to divide the range unevenly. The values must increase monotonically from 0 to 1. N : int The number of RGB quantization levels. gamma : float

bad : :mpltype:color, default: transparent The color for invalid values (NaN or masked). under : :mpltype:color, default: color of the lowest value The color for low out-of-range values. over : :mpltype:color, default: color of the highest value The color for high out-of-range values.

cm_ow = <matplotlib.colors.LinearSegmentedColormap object>
styles: dict = {'default': {'title': '', 'transf': <function <lambda>>, 'cmap': 'viridis', 'levels': 10, 'cticks': None, 'locator': None}, 'oil': {'title': 'Oil saturation', 'transf': <function <lambda>>, 'cmap': <matplotlib.colors.LinearSegmentedColormap object>, 'levels': array([-1.00000000e-07, 5.26314895e-02, 1.05263079e-01, 1.57894668e-01, 2.10526258e-01, 2.63157847e-01, 3.15789437e-01, 3.68421026e-01, 4.21052616e-01, 4.73684205e-01, 5.26315795e-01, 5.78947384e-01, 6.31578974e-01, 6.84210563e-01, 7.36842153e-01, 7.89473742e-01, 8.42105332e-01, 8.94736921e-01, 9.47368511e-01, 1.00000010e+00]), 'cticks': array([0. , 0.2, 0.4, 0.6, 0.8, 1. ])}}

Default Plot2D.plt_field plot styling values.

class Plot2D:
 57class Plot2D:
 58    """Plots specialized for 2D fields.
 59
 60    This mixin is not standalone but reads grid and well attributes of the
 61    `ResSim` it gets composed into, rather than re-declaring (avoids stale).
 62    """
 63
 64    def plt_field(
 65        self: "ResSim",
 66        ax: Any,
 67        Z: np.ndarray,
 68        style: str = "default",
 69        wells: Any = True,
 70        argmax: bool = False,
 71        colorbar: Any = True,
 72        labels: bool = True,
 73        grid: bool = False,
 74        finalize: bool = True,
 75        **kwargs,
 76    ) -> Any:
 77        """Contour-plot of the (flat) unravelled field `Z`.
 78
 79        `kwargs` falls back to `styles[style]`, which falls back to `styles['defaults']`.
 80        """
 81        # Populate kwargs with fallback style
 82        kwargs = {**styles["default"], **styles[style], **kwargs}
 83        # Pop from kwargs. Remainder goes to countourf
 84        ax.set(**axprops(kwargs))
 85        cticks = kwargs.pop("cticks")
 86
 87        # Why extent=(0, Lx, 0, Ly), rather than merely changing ticks?
 88        # set_aspect("equal") and mouse hovering (reporting x,y).
 89        if "rel" in coord_type:
 90            Lx, Ly = 1, 1
 91        elif "abs" in coord_type:
 92            Lx, Ly = self.Lx, self.Ly
 93        elif "ind" in coord_type:
 94            Lx, Ly = self.Nx, self.Ny
 95        else:
 96            raise ValueError(f"Unsupported coord_type: {coord_type}")
 97
 98        # Apply transform
 99        Z = np.asarray(Z)
100        Z = kwargs.pop("transf")(Z)
101
102        # Need to transpose coz orientation is model.shape==(Nx, Ny),
103        # while contour() displays the same orientation as array printing.
104        Z = Z.reshape(self.shape).T
105
106        # Did we bother to specify set_over/set_under/set_bad ?
107        has_out_of_range = getattr(kwargs["cmap"], "_rgba_over", None) is not None
108
109        # Unlike `ax.imshow(Z[::-1])`, `contourf` does not simply fill pixels/cells (but
110        # it does provide nice interpolation!) so there will be whitespace on the margins.
111        # No fix is needed, and anyway it would not be trivial/fast,
112        # ref https://github.com/matplotlib/basemap/issues/406 .
113        collections = ax.contourf(
114            Z,
115            **kwargs,
116            # origin=None,  # ⇒ NB: falsely stretches the field!!!
117            origin="lower",
118            extent=(0, Lx, 0, Ly),
119            extend="both" if has_out_of_range else "neither",
120        )
121
122        # Contourf does not plot (at all) the bad regions. "Fake it" by facecolor
123        if has_out_of_range:
124            ax.set_facecolor(getattr(kwargs["cmap"], "_rgba_bad", "w"))
125
126        # Grid (reflecting the model grid)
127        # NB: If not showing grid, then don't locate ticks on grid, because they're
128        #     generally uglier that mpl's default/automatic tick location. But, it
129        #     should be safe to go with 'g' format instead of 'f'.
130        ax.xaxis.set_major_formatter("{x:g}")
131        ax.yaxis.set_major_formatter("{x:g}")
132        ax.tick_params(which="minor", length=0, color="r")
133        ax.tick_params(which="major", width=1.5, direction="in")
134        if grid:
135            n1 = 10
136            xStep = 1 + self.Nx // n1
137            yStep = 1 + self.Ny // n1
138            ax.xaxis.set_major_locator(MultipleLocator(self.hx * xStep))
139            ax.yaxis.set_major_locator(MultipleLocator(self.hy * yStep))
140            ax.xaxis.set_minor_locator(MultipleLocator(self.hx))
141            ax.yaxis.set_minor_locator(MultipleLocator(self.hy))
142            ax.grid(True, which="both")
143
144        # Axis lims
145        ax.set_xlim((0, Lx))
146        ax.set_ylim((0, Ly))
147        ax.set_aspect("equal")
148        # Axis labels
149        if labels:
150            if "abs" in coord_type:
151                ax.set_xlabel("x")
152                ax.set_ylabel("y")
153            else:
154                ax.set_xlabel(f"x ({coord_type})")
155                ax.set_ylabel(f"y ({coord_type})")
156
157        # Add well markers, grouped (and numbered) by the sign of their rates,
158        # ref `TPFA_ResSim.wells.Wells.signs`. The producers come first, so
159        # that their numbers and colors match those of `plt_production`.
160        if wells and self.wells.nComp:
161            sgn = self.wells.signs
162            # Label the completions by their well's name, if there are any
163            names = None
164            if self.wells.names is not None and self.wells.group is not None:
165                names = np.asarray(self.wells.names)[self.wells.group]
166            if wells == "color":
167                # Colors matching `plt_production` of the producers
168                wells = cast(
169                    dict, {"color": [f"C{i}" for i in range(int(np.sum(sgn < 0)))]}
170                )
171            elif wells in [True, 1]:
172                wells = {}
173            for s in [-1, +1, 0]:
174                if np.any(sgn == s):  # NB: skip, lest empty artists upset the layout
175                    kws = dict(wells)  # NB: copy -- the labels are per sign
176                    if names is not None:
177                        kws.setdefault("text", names[sgn == s])
178                    self.well_scatter(ax, self.wells.xy[sgn == s], s, **kws)
179                wells.pop("color", None)  # producers only
180
181        # Add argmax marker
182        if argmax:
183            idx = Z.T.argmax()  # reverse above transpose
184            xy = self.ind2xy(idx)
185            for c, ms in zip(["b", "r", "y"], [10, 6, 3]):
186                ax.plot(*xy, "o", c=c, ms=ms, label="max", zorder=98)
187
188        # Add colorbar
189        if colorbar:
190            if isinstance(colorbar, type(ax)):
191                cax = dict(cax=colorbar)
192            else:
193                cax = dict(ax=ax, shrink=0.8)
194            ax.figure.colorbar(collections, **cax, ticks=cticks)
195
196        tight_show(ax.figure, finalize)
197        return collections
198
199    def well_scatter(
200        self: "ResSim",
201        ax: Any,
202        ww: np.ndarray,
203        sgn: int = 1,
204        text: Any = None,
205        color: Any = None,  # e.g. "k", or a list of colors (one per well)
206        size: float = 1,
207    ) -> Any:
208        """Scatter-plot the wells of `ww` onto a `Plot2D.plt_field`.
209
210        The marker reflects `sgn`: injector (`+1`), producer (`-1`),
211        or neutral (`0`, i.e. of undecided sign, ref `TPFA_ResSim.wells.Wells.signs`).
212
213        The label, `text`, is either one string for all of them, one *per* well
214        of `ww` (a list), or `False` for none.
215
216        .. note:: The default labels are indices *within* `ww`, not global ones.
217
218            I.e. with `text=None`, and since `plt_field` calls this once per
219            sign, the producers are numbered as `plt_production` numbers them --
220            separately from the injectors, and not as in the unified
221            `TPFA_ResSim.wells.Wells.xy`. But `plt_field` supplies the names of
222            `TPFA_ResSim.wells.Wells.names`, if the wells have been given any.
223        """
224        # Well coordinates
225        ww = self.sub2xy(*self.xy2sub(*ww.T)).T
226        # NB: make sure ww array data is not overwritten (avoid in-place)
227        # fmt: off
228        if   "rel" in coord_type: s = 1/self.Lx, 1/self.Ly                     # noqa
229        elif "abs" in coord_type: s = 1, 1                                     # noqa
230        elif "ind" in coord_type: s = self.Nx/self.Lx, self.Ny/self.Ly         # noqa
231        else: raise ValueError("Unsupported coordinate type: %s" % coord_type) # noqa
232        # fmt: on
233        ww = ww * s
234
235        # Style
236        if sgn > 0:
237            c = "w"
238            ec = "gray"
239            d = "k"
240            m = "v"
241        elif sgn < 0:
242            c = "k"
243            ec = "gray"
244            d = "w"
245            m = "^"
246        else:
247            c = "lightgray"
248            ec = "gray"
249            d = "k"
250            m = "o"
251
252        if color:
253            c = color
254
255        # Markers
256        sh = ax.plot(*ww.T, "r.", ms=3, clip_on=False)
257        sh = ax.scatter(
258            *ww.T,
259            s=(size * 26) ** 2,
260            c=c,
261            marker=m,
262            ec=ec,
263            clip_on=False,
264            zorder=1.5,  # required on Jupypter
265        )
266
267        # Text labels
268        if text is not False:
269            if text is None:
270                labels: Any = range(len(ww))
271            elif isinstance(text, str):
272                labels = len(ww) * [text]
273            else:
274                labels = text
275            for lbl, w in zip(labels, ww):
276                if sgn < 0:
277                    w[1] -= 0.01
278                ax.text(
279                    *w[:2],
280                    lbl,
281                    color=d,
282                    fontsize=size * 12,
283                    ha="center",
284                    va="center",
285                )
286
287        return sh
288
289    def plt_production(
290        self: "ResSim",
291        ax: Any,
292        production: np.ndarray,
293        obs: np.ndarray | None = None,
294        legend_outside: bool = True,
295        finalize: bool = True,
296        labels: Any = None,
297    ) -> list:
298        """Production time series. Multiple wells in 1 axes => not ensemble compat.
299
300        The curves are labelled by their index in `production`, unless `labels`
301        (e.g. a selection of `TPFA_ResSim.wells.Wells.names`) says otherwise.
302        """
303        hh = []
304        tt = 1 + np.arange(len(production))
305        for i, p in enumerate(1 - production.T):
306            hh += ax.plot(tt, p, "-", label=i if labels is None else labels[i])
307
308        if obs is not None:
309            for i, y in enumerate(1 - obs.T):
310                ax.plot(tt, y, "*", c=hh[i].get_color())
311
312        # Add legend
313        if legend_outside:
314            kws = dict(
315                bbox_to_anchor=(1, 1),
316                loc="upper left",
317                ncol=1 + len(production.T) // 10,
318            )
319        else:
320            kws = dict(loc="lower left")
321        ax.legend(title="Well" if labels is not None else "Well #.", **kws)
322
323        ax.set_title("Oil saturation in producers")
324        ax.set_xlabel("Time index")
325        # ax.set_ylim(-0.01, 1.01)
326        ax.axhline(0, c="xkcd:light grey", ls="--", zorder=1.8)
327        ax.axhline(1, c="xkcd:light grey", ls="--", zorder=1.8)
328
329        tight_show(ax.figure, finalize)
330        return hh
331
332    # Note: See note in mpl_setup.py about properly displaying the animation.
333    def anim(
334        self: "ResSim",
335        wsats: np.ndarray,
336        prod: np.ndarray,
337        title: str = "",
338        figsize: tuple = (10, 3.5),
339        pause: int = 200,
340        animate: bool = True,
341        **kwargs,
342    ) -> Any:
343        """Animate the saturation and production time series."""
344
345        # Create figure and axes
346        title = "Animation" + ("-- " + title if title else "")
347        fig, (ax1, ax2) = place.freshfig(
348            title, ncols=2, figsize=figsize, gridspec_kw=dict(width_ratios=(2, 3))
349        )
350        fig.suptitle(title)  # coz animation never (any backend) displays title
351        # Saturations
352        kwargs.update(wells="color", colorbar=True, finalize=False)
353        ax2.cc = self.plt_field(ax2, wsats[-1], "oil", **kwargs)
354        # Production
355        hh = self.plt_production(ax1, prod, legend_outside=False, finalize=False)
356        fig.tight_layout()
357
358        if animate:
359            from matplotlib import animation
360
361            tt = np.arange(len(wsats))
362
363            def update_fig(iT):
364                # Update field.
365                # NB: since mpl 3.8 the `ContourSet` is itself an artist
366                # (it no longer has a `.collections` attribute).
367                try:
368                    ax2.cc.remove()
369                except ValueError:
370                    pass  # occurs when re-running script
371                kwargs.update(wells=False, colorbar=False)
372                ax2.cc = self.plt_field(ax2, wsats[iT], "oil", **kwargs)
373
374                # Update production lines
375                if iT >= 1:
376                    for h, p in zip(hh, prod.T):
377                        h.set_data(tt[1 : 1 + iT], 1 - p[:iT])
378
379            ani = animation.FuncAnimation(
380                fig,
381                update_fig,
382                len(tt),
383                blit=False,
384                interval=pause,
385                # Prevent busy/idle indicator constantly flashing, despite %%capture
386                # and even manually clearing the output of the calling cell.
387                repeat=False,  # flashing stops once the (unshown) animation finishes.
388                # An alternative solution is to do this in the next cell:
389                # animation.event_source.stop()
390                # but it does not work if using "run all", even with time.sleep(1).
391            )
392
393            return ani

Plots specialized for 2D fields.

This mixin is not standalone but reads grid and well attributes of the ResSim it gets composed into, rather than re-declaring (avoids stale).

def plt_field( self: TPFA_ResSim.ResSim, ax: Any, Z: numpy.ndarray, style: str = 'default', wells: Any = True, argmax: bool = False, colorbar: Any = True, labels: bool = True, grid: bool = False, finalize: bool = True, **kwargs) -> Any:
 64    def plt_field(
 65        self: "ResSim",
 66        ax: Any,
 67        Z: np.ndarray,
 68        style: str = "default",
 69        wells: Any = True,
 70        argmax: bool = False,
 71        colorbar: Any = True,
 72        labels: bool = True,
 73        grid: bool = False,
 74        finalize: bool = True,
 75        **kwargs,
 76    ) -> Any:
 77        """Contour-plot of the (flat) unravelled field `Z`.
 78
 79        `kwargs` falls back to `styles[style]`, which falls back to `styles['defaults']`.
 80        """
 81        # Populate kwargs with fallback style
 82        kwargs = {**styles["default"], **styles[style], **kwargs}
 83        # Pop from kwargs. Remainder goes to countourf
 84        ax.set(**axprops(kwargs))
 85        cticks = kwargs.pop("cticks")
 86
 87        # Why extent=(0, Lx, 0, Ly), rather than merely changing ticks?
 88        # set_aspect("equal") and mouse hovering (reporting x,y).
 89        if "rel" in coord_type:
 90            Lx, Ly = 1, 1
 91        elif "abs" in coord_type:
 92            Lx, Ly = self.Lx, self.Ly
 93        elif "ind" in coord_type:
 94            Lx, Ly = self.Nx, self.Ny
 95        else:
 96            raise ValueError(f"Unsupported coord_type: {coord_type}")
 97
 98        # Apply transform
 99        Z = np.asarray(Z)
100        Z = kwargs.pop("transf")(Z)
101
102        # Need to transpose coz orientation is model.shape==(Nx, Ny),
103        # while contour() displays the same orientation as array printing.
104        Z = Z.reshape(self.shape).T
105
106        # Did we bother to specify set_over/set_under/set_bad ?
107        has_out_of_range = getattr(kwargs["cmap"], "_rgba_over", None) is not None
108
109        # Unlike `ax.imshow(Z[::-1])`, `contourf` does not simply fill pixels/cells (but
110        # it does provide nice interpolation!) so there will be whitespace on the margins.
111        # No fix is needed, and anyway it would not be trivial/fast,
112        # ref https://github.com/matplotlib/basemap/issues/406 .
113        collections = ax.contourf(
114            Z,
115            **kwargs,
116            # origin=None,  # ⇒ NB: falsely stretches the field!!!
117            origin="lower",
118            extent=(0, Lx, 0, Ly),
119            extend="both" if has_out_of_range else "neither",
120        )
121
122        # Contourf does not plot (at all) the bad regions. "Fake it" by facecolor
123        if has_out_of_range:
124            ax.set_facecolor(getattr(kwargs["cmap"], "_rgba_bad", "w"))
125
126        # Grid (reflecting the model grid)
127        # NB: If not showing grid, then don't locate ticks on grid, because they're
128        #     generally uglier that mpl's default/automatic tick location. But, it
129        #     should be safe to go with 'g' format instead of 'f'.
130        ax.xaxis.set_major_formatter("{x:g}")
131        ax.yaxis.set_major_formatter("{x:g}")
132        ax.tick_params(which="minor", length=0, color="r")
133        ax.tick_params(which="major", width=1.5, direction="in")
134        if grid:
135            n1 = 10
136            xStep = 1 + self.Nx // n1
137            yStep = 1 + self.Ny // n1
138            ax.xaxis.set_major_locator(MultipleLocator(self.hx * xStep))
139            ax.yaxis.set_major_locator(MultipleLocator(self.hy * yStep))
140            ax.xaxis.set_minor_locator(MultipleLocator(self.hx))
141            ax.yaxis.set_minor_locator(MultipleLocator(self.hy))
142            ax.grid(True, which="both")
143
144        # Axis lims
145        ax.set_xlim((0, Lx))
146        ax.set_ylim((0, Ly))
147        ax.set_aspect("equal")
148        # Axis labels
149        if labels:
150            if "abs" in coord_type:
151                ax.set_xlabel("x")
152                ax.set_ylabel("y")
153            else:
154                ax.set_xlabel(f"x ({coord_type})")
155                ax.set_ylabel(f"y ({coord_type})")
156
157        # Add well markers, grouped (and numbered) by the sign of their rates,
158        # ref `TPFA_ResSim.wells.Wells.signs`. The producers come first, so
159        # that their numbers and colors match those of `plt_production`.
160        if wells and self.wells.nComp:
161            sgn = self.wells.signs
162            # Label the completions by their well's name, if there are any
163            names = None
164            if self.wells.names is not None and self.wells.group is not None:
165                names = np.asarray(self.wells.names)[self.wells.group]
166            if wells == "color":
167                # Colors matching `plt_production` of the producers
168                wells = cast(
169                    dict, {"color": [f"C{i}" for i in range(int(np.sum(sgn < 0)))]}
170                )
171            elif wells in [True, 1]:
172                wells = {}
173            for s in [-1, +1, 0]:
174                if np.any(sgn == s):  # NB: skip, lest empty artists upset the layout
175                    kws = dict(wells)  # NB: copy -- the labels are per sign
176                    if names is not None:
177                        kws.setdefault("text", names[sgn == s])
178                    self.well_scatter(ax, self.wells.xy[sgn == s], s, **kws)
179                wells.pop("color", None)  # producers only
180
181        # Add argmax marker
182        if argmax:
183            idx = Z.T.argmax()  # reverse above transpose
184            xy = self.ind2xy(idx)
185            for c, ms in zip(["b", "r", "y"], [10, 6, 3]):
186                ax.plot(*xy, "o", c=c, ms=ms, label="max", zorder=98)
187
188        # Add colorbar
189        if colorbar:
190            if isinstance(colorbar, type(ax)):
191                cax = dict(cax=colorbar)
192            else:
193                cax = dict(ax=ax, shrink=0.8)
194            ax.figure.colorbar(collections, **cax, ticks=cticks)
195
196        tight_show(ax.figure, finalize)
197        return collections

Contour-plot of the (flat) unravelled field Z.

kwargs falls back to styles[style], which falls back to styles['defaults'].

def well_scatter( self: TPFA_ResSim.ResSim, ax: Any, ww: numpy.ndarray, sgn: int = 1, text: Any = None, color: Any = None, size: float = 1) -> Any:
199    def well_scatter(
200        self: "ResSim",
201        ax: Any,
202        ww: np.ndarray,
203        sgn: int = 1,
204        text: Any = None,
205        color: Any = None,  # e.g. "k", or a list of colors (one per well)
206        size: float = 1,
207    ) -> Any:
208        """Scatter-plot the wells of `ww` onto a `Plot2D.plt_field`.
209
210        The marker reflects `sgn`: injector (`+1`), producer (`-1`),
211        or neutral (`0`, i.e. of undecided sign, ref `TPFA_ResSim.wells.Wells.signs`).
212
213        The label, `text`, is either one string for all of them, one *per* well
214        of `ww` (a list), or `False` for none.
215
216        .. note:: The default labels are indices *within* `ww`, not global ones.
217
218            I.e. with `text=None`, and since `plt_field` calls this once per
219            sign, the producers are numbered as `plt_production` numbers them --
220            separately from the injectors, and not as in the unified
221            `TPFA_ResSim.wells.Wells.xy`. But `plt_field` supplies the names of
222            `TPFA_ResSim.wells.Wells.names`, if the wells have been given any.
223        """
224        # Well coordinates
225        ww = self.sub2xy(*self.xy2sub(*ww.T)).T
226        # NB: make sure ww array data is not overwritten (avoid in-place)
227        # fmt: off
228        if   "rel" in coord_type: s = 1/self.Lx, 1/self.Ly                     # noqa
229        elif "abs" in coord_type: s = 1, 1                                     # noqa
230        elif "ind" in coord_type: s = self.Nx/self.Lx, self.Ny/self.Ly         # noqa
231        else: raise ValueError("Unsupported coordinate type: %s" % coord_type) # noqa
232        # fmt: on
233        ww = ww * s
234
235        # Style
236        if sgn > 0:
237            c = "w"
238            ec = "gray"
239            d = "k"
240            m = "v"
241        elif sgn < 0:
242            c = "k"
243            ec = "gray"
244            d = "w"
245            m = "^"
246        else:
247            c = "lightgray"
248            ec = "gray"
249            d = "k"
250            m = "o"
251
252        if color:
253            c = color
254
255        # Markers
256        sh = ax.plot(*ww.T, "r.", ms=3, clip_on=False)
257        sh = ax.scatter(
258            *ww.T,
259            s=(size * 26) ** 2,
260            c=c,
261            marker=m,
262            ec=ec,
263            clip_on=False,
264            zorder=1.5,  # required on Jupypter
265        )
266
267        # Text labels
268        if text is not False:
269            if text is None:
270                labels: Any = range(len(ww))
271            elif isinstance(text, str):
272                labels = len(ww) * [text]
273            else:
274                labels = text
275            for lbl, w in zip(labels, ww):
276                if sgn < 0:
277                    w[1] -= 0.01
278                ax.text(
279                    *w[:2],
280                    lbl,
281                    color=d,
282                    fontsize=size * 12,
283                    ha="center",
284                    va="center",
285                )
286
287        return sh

Scatter-plot the wells of ww onto a Plot2D.plt_field.

The marker reflects sgn: injector (+1), producer (-1), or neutral (0, i.e. of undecided sign, ref TPFA_ResSim.wells.Wells.signs).

The label, text, is either one string for all of them, one per well of ww (a list), or False for none.

The default labels are indices within ww, not global ones.

I.e. with text=None, and since plt_field calls this once per sign, the producers are numbered as plt_production numbers them -- separately from the injectors, and not as in the unified TPFA_ResSim.wells.Wells.xy. But plt_field supplies the names of TPFA_ResSim.wells.Wells.names, if the wells have been given any.

def plt_production( self: TPFA_ResSim.ResSim, ax: Any, production: numpy.ndarray, obs: numpy.ndarray | None = None, legend_outside: bool = True, finalize: bool = True, labels: Any = None) -> list:
289    def plt_production(
290        self: "ResSim",
291        ax: Any,
292        production: np.ndarray,
293        obs: np.ndarray | None = None,
294        legend_outside: bool = True,
295        finalize: bool = True,
296        labels: Any = None,
297    ) -> list:
298        """Production time series. Multiple wells in 1 axes => not ensemble compat.
299
300        The curves are labelled by their index in `production`, unless `labels`
301        (e.g. a selection of `TPFA_ResSim.wells.Wells.names`) says otherwise.
302        """
303        hh = []
304        tt = 1 + np.arange(len(production))
305        for i, p in enumerate(1 - production.T):
306            hh += ax.plot(tt, p, "-", label=i if labels is None else labels[i])
307
308        if obs is not None:
309            for i, y in enumerate(1 - obs.T):
310                ax.plot(tt, y, "*", c=hh[i].get_color())
311
312        # Add legend
313        if legend_outside:
314            kws = dict(
315                bbox_to_anchor=(1, 1),
316                loc="upper left",
317                ncol=1 + len(production.T) // 10,
318            )
319        else:
320            kws = dict(loc="lower left")
321        ax.legend(title="Well" if labels is not None else "Well #.", **kws)
322
323        ax.set_title("Oil saturation in producers")
324        ax.set_xlabel("Time index")
325        # ax.set_ylim(-0.01, 1.01)
326        ax.axhline(0, c="xkcd:light grey", ls="--", zorder=1.8)
327        ax.axhline(1, c="xkcd:light grey", ls="--", zorder=1.8)
328
329        tight_show(ax.figure, finalize)
330        return hh

Production time series. Multiple wells in 1 axes => not ensemble compat.

The curves are labelled by their index in production, unless labels (e.g. a selection of TPFA_ResSim.wells.Wells.names) says otherwise.

def anim( self: TPFA_ResSim.ResSim, wsats: numpy.ndarray, prod: numpy.ndarray, title: str = '', figsize: tuple = (10, 3.5), pause: int = 200, animate: bool = True, **kwargs) -> Any:
333    def anim(
334        self: "ResSim",
335        wsats: np.ndarray,
336        prod: np.ndarray,
337        title: str = "",
338        figsize: tuple = (10, 3.5),
339        pause: int = 200,
340        animate: bool = True,
341        **kwargs,
342    ) -> Any:
343        """Animate the saturation and production time series."""
344
345        # Create figure and axes
346        title = "Animation" + ("-- " + title if title else "")
347        fig, (ax1, ax2) = place.freshfig(
348            title, ncols=2, figsize=figsize, gridspec_kw=dict(width_ratios=(2, 3))
349        )
350        fig.suptitle(title)  # coz animation never (any backend) displays title
351        # Saturations
352        kwargs.update(wells="color", colorbar=True, finalize=False)
353        ax2.cc = self.plt_field(ax2, wsats[-1], "oil", **kwargs)
354        # Production
355        hh = self.plt_production(ax1, prod, legend_outside=False, finalize=False)
356        fig.tight_layout()
357
358        if animate:
359            from matplotlib import animation
360
361            tt = np.arange(len(wsats))
362
363            def update_fig(iT):
364                # Update field.
365                # NB: since mpl 3.8 the `ContourSet` is itself an artist
366                # (it no longer has a `.collections` attribute).
367                try:
368                    ax2.cc.remove()
369                except ValueError:
370                    pass  # occurs when re-running script
371                kwargs.update(wells=False, colorbar=False)
372                ax2.cc = self.plt_field(ax2, wsats[iT], "oil", **kwargs)
373
374                # Update production lines
375                if iT >= 1:
376                    for h, p in zip(hh, prod.T):
377                        h.set_data(tt[1 : 1 + iT], 1 - p[:iT])
378
379            ani = animation.FuncAnimation(
380                fig,
381                update_fig,
382                len(tt),
383                blit=False,
384                interval=pause,
385                # Prevent busy/idle indicator constantly flashing, despite %%capture
386                # and even manually clearing the output of the calling cell.
387                repeat=False,  # flashing stops once the (unshown) animation finishes.
388                # An alternative solution is to do this in the next cell:
389                # animation.event_source.stop()
390                # but it does not work if using "run all", even with time.sleep(1).
391            )
392
393            return ani

Animate the saturation and production time series.

def tight_show(figure: Any, enabled: bool) -> None:
396def tight_show(figure: Any, enabled: bool) -> None:
397    if enabled:
398        figure.tight_layout()
399        plt.show()
def show(block: Optional[bool] = None) -> None:
421def show(block: Optional[bool] = None) -> None:
422    """Display the figures, whether run as script, in IPython, or in a notebook.
423
424    Reasons why a plain `plt.show()` does not suffice:
425
426    - In a script (`python script.py`) it does the right thing (blocks, thereby
427      running the GUI event loop) *only* if interactive mode (`plt.ion()`) is off.
428    - In IPython (e.g. `%run script.py`) the figures stay unpainted until the GUI
429      event loop gets to run, which requires the "input hook" installed by the
430      `%matplotlib` magic (which is why they only appear upon `ctrl-d`/exit).
431      Blocking here would be wrong: it would freeze the prompt.
432    - In Jupyter/Colab with the `inline` backend there is no event loop at all:
433      the (static) figures are rendered by `plt.show()` itself.
434    """
435    ip = _get_ipython()
436
437    if ip is not None and not is_inline():
438        # I.e. the `%matplotlib` magic: activate the event loop ("input hook")
439        # of the current backend. No-op if already active.
440        ip.run_line_magic("matplotlib", "")
441
442    if block is None:
443        # Run the event loop ourselves only if nobody else will.
444        block = not is_inline() and not _ipython_will_prompt(ip)
445
446    plt.show(block=block)

Display the figures, whether run as script, in IPython, or in a notebook.

Reasons why a plain plt.show() does not suffice:

  • In a script (python script.py) it does the right thing (blocks, thereby running the GUI event loop) only if interactive mode (plt.ion()) is off.
  • In IPython (e.g. %run script.py) the figures stay unpainted until the GUI event loop gets to run, which requires the "input hook" installed by the %matplotlib magic (which is why they only appear upon ctrl-d/exit). Blocking here would be wrong: it would freeze the prompt.
  • In Jupyter/Colab with the inline backend there is no event loop at all: the (static) figures are rendered by plt.show() itself.