From 2269e56139d9ff30dd53689e8a3d0f6e90f25b36 Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 12:47:29 +0200 Subject: [PATCH 1/6] Order/title plot_transient_results by (x, layer) for cross-section models Cross-section models (ModelXsection) have a physically meaningful x coordinate, so subplots are now ordered by (x, layer) and titled with the observation's location and layer - matching the notebook helper plot_headsT this method is meant to replace. Other model types (e.g. areal ModelMaq) fall back to ordering/titling by observation name, since x alone isn't a meaningful ordering there. Also adds an optional `units` dict (same convention as plots.xsection's units=) to label the head/time axes, e.g. units={"head": "m", "time": "days"}. Co-Authored-By: Claude Sonnet 5 --- timflow/calibrate.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index 64693f48..16453fef 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1447,6 +1447,7 @@ def plot_transient_results( obs_kwargs: dict | None = None, model_kwargs: dict | None = None, sharey: bool = False, + units: dict | None = None, ) -> tuple[plt.Figure, np.ndarray]: """Plot modeled vs observed transient head time series. @@ -1455,6 +1456,11 @@ def plot_transient_results( method before calibration to inspect the initial fit, or after calibration to inspect the calibrated fit. + For a cross-section model (``ModelXsection``), subplots are ordered + by ``(x, layer)`` and titled with the observation's location and + layer. For other model types, where ``x`` alone is not a meaningful + ordering, subplots are ordered and titled by observation name instead. + Parameters ---------- tmin : float, optional @@ -1474,6 +1480,10 @@ def plot_transient_results( sharey : bool, optional If ``True``, all subplots share the same y-axis limits. Default is ``False``. + units : dict, optional + Optional units to append to the axis labels, e.g. + ``{"head": "m", "time": "days"}``. Same ``dict``-of-units + convention used by :func:`timflow.plots.plots.xsection`. Returns ------- @@ -1498,6 +1508,22 @@ def plot_transient_results( if n_obs == 0: raise ValueError("No transient observations to plot.") + # Cross-section models have a meaningful x-coordinate to sort/title + # on; for other model types (e.g. areal models) fall back to the + # observation name instead. + _model_for_type = ( + self.transient_model if self.transient_model is not None else self.steady_model + ) + is_xsection = type(_model_for_type).__name__ == "ModelXsection" + if is_xsection: + all_items = sorted(all_items, key=lambda item: (item[1].x, item[1].layer)) + else: + all_items = sorted(all_items, key=lambda item: item[0]) + + units = units or {} + head_unit = f" [{units['head']}]" if "head" in units else "" + time_unit = f" [{units['time']}]" if "time" in units else "" + # Default styling obs_kw: dict = {"color": "k", "marker": ".", "linestyle": "none"} obs_kw.update(obs_kwargs or {}) @@ -1575,11 +1601,15 @@ def plot_transient_results( model_kw["color"] = f"C{i}" # cycle through colors for each subplot ax.plot(t_plot[mask], h_obs_plot[mask], label=obs_label, **obs_kw) ax.plot(t_plot[mask], h_mod[mask], **{**model_kw, "label": model_label}) - ax.set_ylabel("head") + if is_xsection: + ax.set_title(f"{name}: x={obs.x:.1f}, layer={obs.layer}", loc="right") + else: + ax.set_title(name, loc="right") + ax.set_ylabel(f"head{head_unit}") ax.grid(True) ax.legend(loc=(0, 1), frameon=False, ncol=2) ax.set_xlim(left=t_plot[mask][0], right=t_plot[mask][-1]) i += 1 - axes[-1].set_xlabel("time") + axes[-1].set_xlabel(f"time{time_unit}") fig.tight_layout() return fig, axes From f2bf049a0c1a69fdb5ea90d4454136be0b651d95 Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 12:59:12 +0200 Subject: [PATCH 2/6] add units to x value --- timflow/calibrate.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index 16453fef..fec7affe 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1481,9 +1481,9 @@ def plot_transient_results( If ``True``, all subplots share the same y-axis limits. Default is ``False``. units : dict, optional - Optional units to append to the axis labels, e.g. - ``{"head": "m", "time": "days"}``. Same ``dict``-of-units - convention used by :func:`timflow.plots.plots.xsection`. + Optional units to append to the axis labels and, for + cross-section models, the per-subplot ``x`` value in the title, + e.g. ``{"head": "m", "time": "days", "x": "m"}``. Returns ------- @@ -1523,6 +1523,7 @@ def plot_transient_results( units = units or {} head_unit = f" [{units['head']}]" if "head" in units else "" time_unit = f" [{units['time']}]" if "time" in units else "" + x_unit = f" [{units['x']}]" if "x" in units else "" # Default styling obs_kw: dict = {"color": "k", "marker": ".", "linestyle": "none"} @@ -1601,10 +1602,15 @@ def plot_transient_results( model_kw["color"] = f"C{i}" # cycle through colors for each subplot ax.plot(t_plot[mask], h_obs_plot[mask], label=obs_label, **obs_kw) ax.plot(t_plot[mask], h_mod[mask], **{**model_kw, "label": model_label}) + if is_xsection: - ax.set_title(f"{name}: x={obs.x:.1f}, layer={obs.layer}", loc="right") + ax.set_title( + f"x: {obs.x:.1f}{x_unit}, layer: {obs.layer}", + loc="right", + fontsize="medium", + ) else: - ax.set_title(name, loc="right") + ax.set_title(f"layer: {obs.layer}", loc="right", fontsize="medium") ax.set_ylabel(f"head{head_unit}") ax.grid(True) ax.legend(loc=(0, 1), frameon=False, ncol=2) From 310556b2c9b2d7460b910ea0ddd4df0a35a88f8e Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 13:03:04 +0200 Subject: [PATCH 3/6] ruff and remove brackets --- timflow/calibrate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index fec7affe..8eadce39 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1512,7 +1512,9 @@ def plot_transient_results( # on; for other model types (e.g. areal models) fall back to the # observation name instead. _model_for_type = ( - self.transient_model if self.transient_model is not None else self.steady_model + self.transient_model + if self.transient_model is not None + else self.steady_model ) is_xsection = type(_model_for_type).__name__ == "ModelXsection" if is_xsection: @@ -1523,7 +1525,7 @@ def plot_transient_results( units = units or {} head_unit = f" [{units['head']}]" if "head" in units else "" time_unit = f" [{units['time']}]" if "time" in units else "" - x_unit = f" [{units['x']}]" if "x" in units else "" + x_unit = f" {units['x']}" if "x" in units else "" # Default styling obs_kw: dict = {"color": "k", "marker": ".", "linestyle": "none"} From 5909e6fda343c1e332c0298cd64d8a3e9e98bf18 Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 13:10:41 +0200 Subject: [PATCH 4/6] align y-axis labels --- timflow/calibrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index 8eadce39..e30d9d2e 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1619,5 +1619,6 @@ def plot_transient_results( ax.set_xlim(left=t_plot[mask][0], right=t_plot[mask][-1]) i += 1 axes[-1].set_xlabel(f"time{time_unit}") + fig.align_ylabels(axes) fig.tight_layout() return fig, axes From 4326881ee3463f3c9568ed13c873968225b3d66c Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 13:52:33 +0200 Subject: [PATCH 5/6] switch layout to constrained --- timflow/calibrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index e30d9d2e..af5eb241 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1544,7 +1544,7 @@ def plot_transient_results( # Create subplots fig, ax_array = plt.subplots( - n_obs, 1, sharex=True, sharey=sharey, figsize=figsize + n_obs, 1, sharex=True, sharey=sharey, figsize=figsize, layout="constrained" ) axes = np.atleast_1d(ax_array) @@ -1620,5 +1620,5 @@ def plot_transient_results( i += 1 axes[-1].set_xlabel(f"time{time_unit}") fig.align_ylabels(axes) - fig.tight_layout() + # fig.tight_layout() return fig, axes From ef71dbb28a06edc3cc59a7982e11aff052d826dd Mon Sep 17 00:00:00 2001 From: Mattijs Borst Date: Fri, 25 Sep 2026 14:12:01 +0200 Subject: [PATCH 6/6] remove old code --- timflow/calibrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/timflow/calibrate.py b/timflow/calibrate.py index af5eb241..bcbbaee4 100644 --- a/timflow/calibrate.py +++ b/timflow/calibrate.py @@ -1620,5 +1620,4 @@ def plot_transient_results( i += 1 axes[-1].set_xlabel(f"time{time_unit}") fig.align_ylabels(axes) - # fig.tight_layout() return fig, axes