import numpy as np
from xpcsviewer.plothandler.plot_constants import BASIC_COLORS as colors
from xpcsviewer.plothandler.plot_constants import EXTENDED_MARKERS as shapes
from xpcsviewer.utils.exceptions import XPCSValidationError
from xpcsviewer.utils.logging_config import get_logger
from xpcsviewer.utils.validation import (
get_file_label_safe,
validate_array_compatibility,
validate_xf_fit_summary,
)
logger = get_logger(__name__)
[docs]
def plot(xf_list, hdl, q_range, offset, plot_type=3):
"""Plot relaxation time tau versus scattering vector q.
Renders tau(q) with error bars for each file, optionally
overlaying a power-law fit line when available.
Args:
xf_list: List of XpcsFile objects that contain G2 fitting
results (``fit_summary``).
hdl: Matplotlib figure handle (``MplCanvas``) for rendering.
q_range: Tuple ``(qmin, qmax)`` limiting the q-axis display
range (currently unused inside the function but passed
for caller compatibility).
offset: Decade offset exponent applied per file for visual
separation. File *n* is divided by ``10^(offset * n)``.
plot_type: Axis scale encoding. 0 = lin-lin, 1 = log-lin,
2 = lin-log, 3 = log-log. Default is 3.
Example:
>>> plot(xf_list, hdl=canvas, q_range=(0.001, 0.1),
... offset=0, plot_type=3)
"""
logger.info(f"Starting tau-q plot for {len(xf_list)} files")
logger.debug(
f"Plot parameters: q_range={q_range}, offset={offset}, plot_type={plot_type}"
)
hdl.clear()
ax = hdl.subplots(1, 1)
# Pre-compute scale factors and colors/shapes to avoid repeated calculations
num_files = len(xf_list)
scale_factors = np.array([10 ** (offset * n) for n in range(num_files)])
color_cycle = [colors[n % len(colors)] for n in range(num_files)]
shape_cycle = [shapes[n % len(shapes)] for n in range(num_files)]
plot_count = 0
for n, xf in enumerate(xf_list):
# Extract data with error checking using validation utility
is_valid, fit_summary, _error_msg = validate_xf_fit_summary(xf)
if not is_valid:
continue
s = scale_factors[n]
# Prefer filtered tauq data (validity-checked, within q_range)
# produced by fit_tauq. Fall back to full G2 fit_val when tauq
# keys are absent (e.g. fit_tauq returned early with no valid data).
if "tauq_q" in fit_summary:
x = np.asarray(fit_summary["tauq_q"])
y = np.asarray(fit_summary["tauq_tau"])
e = np.asarray(fit_summary["tauq_tau_err"])
else:
x = fit_summary["q_val"]
if not hasattr(x, "shape"):
x = np.array(x)
y = fit_summary["fit_val"][:, 0, 1]
e = fit_summary["fit_val"][:, 1, 1]
# Validate all arrays have the same length (raises on mismatch)
file_label = get_file_label_safe(xf)
try:
validate_array_compatibility(x, y, e, file_label=file_label)
except XPCSValidationError as exc:
logger.warning(f"Skipping file {file_label}: {exc}")
continue
# Filter invalid points (redundant for tauq path but needed
# for the fit_val fallback where sigma=0 marks failed fits)
valid_idx = e > 0
if not np.any(valid_idx):
logger.debug(
f"Skipping file {getattr(xf, 'label', 'unknown')} - no valid data points"
)
continue
x_valid = x[valid_idx]
y_valid = y[valid_idx]
e_valid = e[valid_idx]
# Pre-computed color and shape
color = color_cycle[n]
shape = shape_cycle[n]
# Plot with pre-computed scaled values
y_scaled = y_valid / s
e_scaled = e_valid / s
ax.errorbar(
x_valid,
y_scaled,
yerr=e_scaled,
fmt=shape,
markersize=3,
label=xf.label,
color=color,
mfc="white",
)
plot_count += 1
# Plot fit line if available
if fit_summary.get("tauq_success", False):
tauq_fit_line = fit_summary.get("tauq_fit_line")
if (
isinstance(tauq_fit_line, dict)
and "fit_x" in tauq_fit_line
and "fit_y" in tauq_fit_line
):
fit_x = tauq_fit_line["fit_x"]
fit_y = tauq_fit_line["fit_y"]
ax.plot(fit_x, fit_y / s, color=color)
# Set labels and scales
ax.set_xlabel("$q (\\AA^{-1})$")
ax.set_ylabel("$\\tau (s)$")
ax.legend()
# Use pre-computed scale options
scale_options = ["linear", "log"]
xscale = scale_options[plot_type % 2]
yscale = scale_options[plot_type // 2]
ax.set_xscale(xscale)
ax.set_yscale(yscale)
hdl.draw()
logger.info(f"Tau-q plot completed successfully - {plot_count} plots drawn")
[docs]
def plot_pre(xf_list, hdl):
"""Plot G2 fitting parameters versus q in a 2x2 subplot grid.
Displays contrast, tau, stretch exponent, and baseline as
functions of scattering vector q for all files in *xf_list*.
Upper and lower fit bounds are shown as horizontal lines.
When *xf_list* is empty, an instructional message is displayed
directing the user to perform G2 fitting first.
Args:
xf_list: List of XpcsFile objects with G2 fitting results
(``fit_summary`` containing ``q_val``, ``fit_val``,
and ``bounds``).
hdl: Matplotlib figure handle (``MplCanvas``) for rendering.
Example:
>>> plot_pre(xf_list, hdl=canvas)
"""
logger.info(f"Starting tau-q pre-plot for {len(xf_list)} files")
hdl.clear()
# Handle empty file list case
if len(xf_list) == 0:
logger.warning(
"No files provided for tau-q pre-plot - showing instruction message"
)
ax = hdl.subplots(1, 1)
ax.text(
0.5,
0.5,
'No files with G2 fitting results available.\n\nTo see diffusion analysis:\n1. Go to "g2" tab\n2. Select files and click "plot" to fit G2\n3. Return to "Diffusion" tab',
ha="center",
va="center",
fontsize=12,
bbox={"boxstyle": "round,pad=0.3", "facecolor": "lightblue", "alpha": 0.7},
)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
hdl.fig.tight_layout()
hdl.draw()
logger.info("Displayed instruction message for missing G2 fitting results")
return
logger.info(f"Creating 2x2 subplot layout for {len(xf_list)} files")
ax = hdl.subplots(2, 2, sharex=True).flatten()
titles = ["contrast", "tau (s)", "stretch", "baseline"]
# Pre-compute colors and shapes for all files
color_cycle = [colors[idx % len(colors)] for idx in range(len(xf_list))]
shape_cycle = [shapes[idx % len(shapes)] for idx in range(len(xf_list))]
# Track bounds for final setup
global_bounds = None
x_range = None
processed_files = 0
for idx, xf in enumerate(xf_list):
try:
# Validate file using validation utility
is_valid, fit_summary, _error_msg = validate_xf_fit_summary(xf)
if not is_valid:
continue
color = color_cycle[idx]
shape = shape_cycle[idx]
x = fit_summary["q_val"]
fit_val = fit_summary["fit_val"]
# Ensure x is a numpy array (convert from list if necessary)
if not hasattr(x, "shape"):
x = np.array(x)
# Validate array dimensions before plotting
if len(x.shape) > 1:
x = x.flatten()
# Ensure x and fit_val have compatible shapes
min_length = min(len(x), fit_val.shape[0])
x = x[:min_length]
fit_val = fit_val[:min_length]
# Plot all parameters at once with vectorized operations
for n in range(4):
y = fit_val[:, 0, n]
e = fit_val[:, 1, n]
# Additional safety check for array lengths
if len(x) != len(y):
logger.warning(
f"Size mismatch: x={len(x)}, y={len(y)}. Truncating to shorter length."
)
min_len = min(len(x), len(y))
x_plot = x[:min_len]
y_plot = y[:min_len]
e_plot = e[:min_len]
else:
x_plot = x
y_plot = y
e_plot = e
# Skip plotting if arrays are empty
if len(x_plot) == 0 or len(y_plot) == 0:
logger.warning(
f"Empty arrays for file {getattr(xf, 'label', 'unknown')}, parameter {n}"
)
continue
ax[n].errorbar(
x_plot,
y_plot,
yerr=e_plot,
fmt=shape,
markersize=3,
color=color,
mfc="white",
)
# Store bounds and x range from the last valid file
if "bounds" in fit_summary:
global_bounds = fit_summary["bounds"]
x_range = (np.min(x), np.max(x))
processed_files += 1
except Exception as e:
logger.error(f"Error plotting file {getattr(xf, 'label', 'unknown')}: {e}")
continue
# Set up axes after all data is plotted
if global_bounds is not None and x_range is not None:
xmin, xmax = x_range
for n in range(4):
ymin = global_bounds[0][n]
ymax = global_bounds[1][n]
# Set titles and scales
ax[n].set_title(titles[n])
if n == 1: # tau parameter gets log scale
ax[n].set_yscale("log")
# Set y limits with buffer
ax[n].set_ylim(ymin * 0.8, ymax * 1.2)
# Set x labels for bottom row
if n > 1:
ax[n].set_xlabel("$q (\\AA^{-1})$")
# Add bound lines
ax[n].hlines(ymin, xmin, xmax, color="b", label="lower bound")
ax[n].hlines(ymax, xmin, xmax, color="g", label="upper bound")
# Show legend only in the last plot
if n == 3:
ax[n].legend()
logger.info(f"Pre-plot final setup: processed {processed_files} files successfully")
logger.info(f"Global bounds available: {global_bounds is not None}")
hdl.draw()
logger.info(
f"Tau-q pre-plot completed successfully - processed {processed_files}/{len(xf_list)} files"
)
return